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 java.io.ByteArrayInputStream;
023 import java.io.ByteArrayOutputStream;
024 import java.io.File;
025 import java.io.IOException;
026 import java.io.StringReader;
027 import java.io.UnsupportedEncodingException;
028 import java.net.MalformedURLException;
029 import java.net.URL;
030 import java.net.URLClassLoader;
031 import java.util.ArrayList;
032 import java.util.HashMap;
033 import java.util.LinkedList;
034 import java.util.List;
035 import java.util.Map;
036 import java.util.Stack;
037 import java.util.regex.Matcher;
038 import java.util.regex.Pattern;
039
040 import javax.swing.text.MutableAttributeSet;
041 import javax.swing.text.html.HTML;
042 import javax.swing.text.html.HTMLEditorKit;
043 import javax.swing.text.html.parser.ParserDelegator;
044
045 import org.apache.maven.artifact.DependencyResolutionRequiredException;
046 import org.apache.maven.model.Dependency;
047 import org.apache.maven.plugin.descriptor.MojoDescriptor;
048 import org.apache.maven.plugin.descriptor.PluginDescriptor;
049 import org.apache.maven.project.MavenProject;
050 import org.apache.maven.reporting.MavenReport;
051 import org.codehaus.plexus.component.repository.ComponentDependency;
052 import org.codehaus.plexus.util.StringUtils;
053 import org.codehaus.plexus.util.xml.XMLWriter;
054 import org.w3c.tidy.Tidy;
055
056 /**
057 * Convenience methods to play with Maven plugins.
058 *
059 * @author jdcasey
060 * @version $Id: GeneratorUtils.java 1342928 2012-05-26 17:12:27Z hboutemy $
061 */
062 public final class GeneratorUtils
063 {
064 private GeneratorUtils()
065 {
066 // nop
067 }
068
069 /**
070 * @param w not null writer
071 * @param pluginDescriptor not null
072 */
073 public static void writeDependencies( XMLWriter w, PluginDescriptor pluginDescriptor )
074 {
075 w.startElement( "dependencies" );
076
077 @SuppressWarnings( "unchecked" )
078 List<ComponentDependency> deps = pluginDescriptor.getDependencies();
079 for ( ComponentDependency dep : deps )
080 {
081 w.startElement( "dependency" );
082
083 element( w, "groupId", dep.getGroupId() );
084
085 element( w, "artifactId", dep.getArtifactId() );
086
087 element( w, "type", dep.getType() );
088
089 element( w, "version", dep.getVersion() );
090
091 w.endElement();
092 }
093
094 w.endElement();
095 }
096
097 /**
098 * @param w not null writer
099 * @param name not null
100 * @param value could be null
101 */
102 public static void element( XMLWriter w, String name, String value )
103 {
104 w.startElement( name );
105
106 if ( value == null )
107 {
108 value = "";
109 }
110
111 w.writeText( value );
112
113 w.endElement();
114 }
115
116 public static void element( XMLWriter w, String name, String value, boolean asText )
117 {
118 element( w, name, asText ? GeneratorUtils.toText( value ) : value );
119 }
120
121 /**
122 * @param dependencies not null list of <code>Dependency</code>
123 * @return list of component dependencies
124 */
125 public static List<ComponentDependency> toComponentDependencies( List<Dependency> dependencies )
126 {
127 List<ComponentDependency> componentDeps = new LinkedList<ComponentDependency>();
128
129 for ( Dependency dependency : dependencies )
130 {
131 ComponentDependency cd = new ComponentDependency();
132
133 cd.setArtifactId( dependency.getArtifactId() );
134 cd.setGroupId( dependency.getGroupId() );
135 cd.setVersion( dependency.getVersion() );
136 cd.setType( dependency.getType() );
137
138 componentDeps.add( cd );
139 }
140
141 return componentDeps;
142 }
143
144 /**
145 * Returns a literal replacement <code>String</code> for the specified <code>String</code>. This method
146 * produces a <code>String</code> that will work as a literal replacement <code>s</code> in the
147 * <code>appendReplacement</code> method of the {@link Matcher} class. The <code>String</code> produced will
148 * match the sequence of characters in <code>s</code> treated as a literal sequence. Slashes ('\') and dollar
149 * signs ('$') will be given no special meaning. TODO: copied from Matcher class of Java 1.5, remove once target
150 * platform can be upgraded
151 *
152 * @see <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Matcher.html">java.util.regex.Matcher</a>
153 * @param s The string to be literalized
154 * @return A literal string replacement
155 */
156 private static String quoteReplacement( String s )
157 {
158 if ( ( s.indexOf( '\\' ) == -1 ) && ( s.indexOf( '$' ) == -1 ) )
159 {
160 return s;
161 }
162
163 StringBuilder sb = new StringBuilder();
164 for ( int i = 0; i < s.length(); i++ )
165 {
166 char c = s.charAt( i );
167 if ( c == '\\' )
168 {
169 sb.append( '\\' );
170 sb.append( '\\' );
171 }
172 else if ( c == '$' )
173 {
174 sb.append( '\\' );
175 sb.append( '$' );
176 }
177 else
178 {
179 sb.append( c );
180 }
181 }
182
183 return sb.toString();
184 }
185
186 /**
187 * Decodes javadoc inline tags into equivalent HTML tags. For instance, the inline tag "{@code <A&B>}" should be
188 * rendered as "<code><A&B></code>".
189 *
190 * @param description The javadoc description to decode, may be <code>null</code>.
191 * @return The decoded description, never <code>null</code>.
192 */
193 static String decodeJavadocTags( String description )
194 {
195 if ( StringUtils.isEmpty( description ) )
196 {
197 return "";
198 }
199
200 StringBuffer decoded = new StringBuffer( description.length() + 1024 );
201
202 Matcher matcher = Pattern.compile( "\\{@(\\w+)\\s*([^\\}]*)\\}" ).matcher( description );
203 while ( matcher.find() )
204 {
205 String tag = matcher.group( 1 );
206 String text = matcher.group( 2 );
207 text = StringUtils.replace( text, "&", "&" );
208 text = StringUtils.replace( text, "<", "<" );
209 text = StringUtils.replace( text, ">", ">" );
210 if ( "code".equals( tag ) )
211 {
212 text = "<code>" + text + "</code>";
213 }
214 else if ( "link".equals( tag ) || "linkplain".equals( tag ) || "value".equals( tag ) )
215 {
216 String pattern = "(([^#\\.\\s]+\\.)*([^#\\.\\s]+))?" + "(#([^\\(\\s]*)(\\([^\\)]*\\))?\\s*(\\S.*)?)?";
217 final int label = 7;
218 final int clazz = 3;
219 final int member = 5;
220 final int args = 6;
221 Matcher link = Pattern.compile( pattern ).matcher( text );
222 if ( link.matches() )
223 {
224 text = link.group( label );
225 if ( StringUtils.isEmpty( text ) )
226 {
227 text = link.group( clazz );
228 if ( StringUtils.isEmpty( text ) )
229 {
230 text = "";
231 }
232 if ( StringUtils.isNotEmpty( link.group( member ) ) )
233 {
234 if ( StringUtils.isNotEmpty( text ) )
235 {
236 text += '.';
237 }
238 text += link.group( member );
239 if ( StringUtils.isNotEmpty( link.group( args ) ) )
240 {
241 text += "()";
242 }
243 }
244 }
245 }
246 if ( !"linkplain".equals( tag ) )
247 {
248 text = "<code>" + text + "</code>";
249 }
250 }
251 matcher.appendReplacement( decoded, ( text != null ) ? quoteReplacement( text ) : "" );
252 }
253 matcher.appendTail( decoded );
254
255 return decoded.toString();
256 }
257
258 /**
259 * Fixes some javadoc comment to become a valid XHTML snippet.
260 *
261 * @param description Javadoc description with HTML tags, may be <code>null</code>.
262 * @return The description with valid XHTML tags, never <code>null</code>.
263 */
264 public static String makeHtmlValid( String description )
265 {
266 if ( StringUtils.isEmpty( description ) )
267 {
268 return "";
269 }
270
271 String commentCleaned = decodeJavadocTags( description );
272
273 // Using jTidy to clean comment
274 Tidy tidy = new Tidy();
275 tidy.setDocType( "loose" );
276 tidy.setXHTML( true );
277 tidy.setXmlOut( true );
278 tidy.setInputEncoding( "UTF-8" );
279 tidy.setOutputEncoding( "UTF-8" );
280 tidy.setMakeClean( true );
281 tidy.setNumEntities( true );
282 tidy.setQuoteNbsp( false );
283 tidy.setQuiet( true );
284 tidy.setShowWarnings( false );
285 try
286 {
287 ByteArrayOutputStream out = new ByteArrayOutputStream( commentCleaned.length() + 256 );
288 tidy.parse( new ByteArrayInputStream( commentCleaned.getBytes( "UTF-8" ) ), out );
289 commentCleaned = out.toString( "UTF-8" );
290 }
291 catch ( UnsupportedEncodingException e )
292 {
293 // cannot happen as every JVM must support UTF-8, see also class javadoc for java.nio.charset.Charset
294 }
295
296 if ( StringUtils.isEmpty( commentCleaned ) )
297 {
298 return "";
299 }
300
301 // strip the header/body stuff
302 String ls = System.getProperty( "line.separator" );
303 int startPos = commentCleaned.indexOf( "<body>" + ls ) + 6 + ls.length();
304 int endPos = commentCleaned.indexOf( ls + "</body>" );
305 commentCleaned = commentCleaned.substring( startPos, endPos );
306
307 return commentCleaned;
308 }
309
310 /**
311 * Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain
312 * as much of the text formatting as possible by means of the following transformations:
313 * <ul>
314 * <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and
315 * finally the item contents. Each tab denotes an increase of indentation.</li>
316 * <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline
317 * (U+000A) to denote a mandatory line break.</li>
318 * <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized
319 * to a single space. The resulting space denotes a possible point for line wrapping.</li>
320 * <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li>
321 * </ul>
322 *
323 * @param html The HTML fragment to convert to plain text, may be <code>null</code>.
324 * @return A string with HTML tags converted into pure text, never <code>null</code>.
325 * @since 2.4.3
326 */
327 public static String toText( String html )
328 {
329 if ( StringUtils.isEmpty( html ) )
330 {
331 return "";
332 }
333
334 final StringBuilder sb = new StringBuilder();
335
336 HTMLEditorKit.Parser parser = new ParserDelegator();
337 HTMLEditorKit.ParserCallback htmlCallback = new MojoParserCallback( sb );
338
339 try
340 {
341 parser.parse( new StringReader( makeHtmlValid( html ) ), htmlCallback, true );
342 }
343 catch ( IOException e )
344 {
345 throw new RuntimeException( e );
346 }
347
348 return sb.toString().replace( '\"', '\'' ); // for CDATA
349 }
350
351 /**
352 * ParserCallback implementation.
353 */
354 private static class MojoParserCallback
355 extends HTMLEditorKit.ParserCallback
356 {
357 /**
358 * Holds the index of the current item in a numbered list.
359 */
360 class Counter
361 {
362 public int value;
363 }
364
365 /**
366 * A flag whether the parser is currently in the body element.
367 */
368 private boolean body;
369
370 /**
371 * A flag whether the parser is currently processing preformatted text, actually a counter to track nesting.
372 */
373 private int preformatted;
374
375 /**
376 * The current indentation depth for the output.
377 */
378 private int depth;
379
380 /**
381 * A stack of {@link Counter} objects corresponding to the nesting of (un-)ordered lists. A
382 * <code>null</code> element denotes an unordered list.
383 */
384 private Stack<Counter> numbering = new Stack<Counter>();
385
386 /**
387 * A flag whether an implicit line break is pending in the output buffer. This flag is used to postpone the
388 * output of implicit line breaks until we are sure that are not to be merged with other implicit line
389 * breaks.
390 */
391 private boolean pendingNewline;
392
393 /**
394 * A flag whether we have just parsed a simple tag.
395 */
396 private boolean simpleTag;
397
398 /**
399 * The current buffer.
400 */
401 private final StringBuilder sb;
402
403 /**
404 * @param sb not null
405 */
406 public MojoParserCallback( StringBuilder sb )
407 {
408 this.sb = sb;
409 }
410
411 /** {@inheritDoc} */
412 public void handleSimpleTag( HTML.Tag t, MutableAttributeSet a, int pos )
413 {
414 simpleTag = true;
415 if ( body && HTML.Tag.BR.equals( t ) )
416 {
417 newline( false );
418 }
419 }
420
421 /** {@inheritDoc} */
422 public void handleStartTag( HTML.Tag t, MutableAttributeSet a, int pos )
423 {
424 simpleTag = false;
425 if ( body && ( t.breaksFlow() || t.isBlock() ) )
426 {
427 newline( true );
428 }
429 if ( HTML.Tag.OL.equals( t ) )
430 {
431 numbering.push( new Counter() );
432 }
433 else if ( HTML.Tag.UL.equals( t ) )
434 {
435 numbering.push( null );
436 }
437 else if ( HTML.Tag.LI.equals( t ) )
438 {
439 Counter counter = numbering.peek();
440 if ( counter == null )
441 {
442 text( "-\t" );
443 }
444 else
445 {
446 text( ++counter.value + ".\t" );
447 }
448 depth++;
449 }
450 else if ( HTML.Tag.DD.equals( t ) )
451 {
452 depth++;
453 }
454 else if ( t.isPreformatted() )
455 {
456 preformatted++;
457 }
458 else if ( HTML.Tag.BODY.equals( t ) )
459 {
460 body = true;
461 }
462 }
463
464 /** {@inheritDoc} */
465 public void handleEndTag( HTML.Tag t, int pos )
466 {
467 if ( HTML.Tag.OL.equals( t ) || HTML.Tag.UL.equals( t ) )
468 {
469 numbering.pop();
470 }
471 else if ( HTML.Tag.LI.equals( t ) || HTML.Tag.DD.equals( t ) )
472 {
473 depth--;
474 }
475 else if ( t.isPreformatted() )
476 {
477 preformatted--;
478 }
479 else if ( HTML.Tag.BODY.equals( t ) )
480 {
481 body = false;
482 }
483 if ( body && ( t.breaksFlow() || t.isBlock() ) && !HTML.Tag.LI.equals( t ) )
484 {
485 if ( ( HTML.Tag.P.equals( t ) || HTML.Tag.PRE.equals( t ) || HTML.Tag.OL.equals( t )
486 || HTML.Tag.UL.equals( t ) || HTML.Tag.DL.equals( t ) )
487 && numbering.isEmpty() )
488 {
489 pendingNewline = false;
490 newline( pendingNewline );
491 }
492 else
493 {
494 newline( true );
495 }
496 }
497 }
498
499 /** {@inheritDoc} */
500 public void handleText( char[] data, int pos )
501 {
502 /*
503 * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by
504 * the text event ">..." so we need to watch out for the closing angle bracket.
505 */
506 int offset = 0;
507 if ( simpleTag && data[0] == '>' )
508 {
509 simpleTag = false;
510 for ( ++offset; offset < data.length && data[offset] <= ' '; )
511 {
512 offset++;
513 }
514 }
515 if ( offset < data.length )
516 {
517 String text = new String( data, offset, data.length - offset );
518 text( text );
519 }
520 }
521
522 /** {@inheritDoc} */
523 public void flush()
524 {
525 flushPendingNewline();
526 }
527
528 /**
529 * Writes a line break to the plain text output.
530 *
531 * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are
532 * always written to the output whereas consecutive implicit line breaks are merged into a single
533 * line break.
534 */
535 private void newline( boolean implicit )
536 {
537 if ( implicit )
538 {
539 pendingNewline = true;
540 }
541 else
542 {
543 flushPendingNewline();
544 sb.append( '\n' );
545 }
546 }
547
548 /**
549 * Flushes a pending newline (if any).
550 */
551 private void flushPendingNewline()
552 {
553 if ( pendingNewline )
554 {
555 pendingNewline = false;
556 if ( sb.length() > 0 )
557 {
558 sb.append( '\n' );
559 }
560 }
561 }
562
563 /**
564 * Writes the specified character data to the plain text output. If the last output was a line break, the
565 * character data will automatically be prefixed with the current indent.
566 *
567 * @param data The character data, must not be <code>null</code>.
568 */
569 private void text( String data )
570 {
571 flushPendingNewline();
572 if ( sb.length() <= 0 || sb.charAt( sb.length() - 1 ) == '\n' )
573 {
574 for ( int i = 0; i < depth; i++ )
575 {
576 sb.append( '\t' );
577 }
578 }
579 String text;
580 if ( preformatted > 0 )
581 {
582 text = data;
583 }
584 else
585 {
586 text = data.replace( '\n', ' ' );
587 }
588 sb.append( text );
589 }
590 }
591
592 /**
593 * Find the best package name, based on the number of hits of actual Mojo classes.
594 *
595 * @param pluginDescriptor not null
596 * @return the best name of the package for the generated mojo
597 */
598 public static String discoverPackageName( PluginDescriptor pluginDescriptor )
599 {
600 Map<String, Integer> packageNames = new HashMap<String, Integer>();
601 @SuppressWarnings( "unchecked" )
602 List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos();
603 if ( mojoDescriptors == null )
604 {
605 return "";
606 }
607 for ( MojoDescriptor descriptor : mojoDescriptors )
608 {
609
610 String impl = descriptor.getImplementation();
611 if ( StringUtils.equals( descriptor.getGoal(), "help" ) && StringUtils.equals( "HelpMojo", impl ) )
612 {
613 continue;
614 }
615 if ( impl.lastIndexOf( '.' ) != -1 )
616 {
617 String name = impl.substring( 0, impl.lastIndexOf( '.' ) );
618 if ( packageNames.get( name ) != null )
619 {
620 int next = ( packageNames.get( name ) ).intValue() + 1;
621 packageNames.put( name, Integer.valueOf( next ) );
622 }
623 else
624 {
625 packageNames.put( name, Integer.valueOf( 1 ) );
626 }
627 }
628 else
629 {
630 packageNames.put( "", Integer.valueOf( 1 ) );
631 }
632 }
633
634 String packageName = "";
635 int max = 0;
636 for ( Map.Entry<String, Integer> entry : packageNames.entrySet() )
637 {
638 int value = entry.getValue().intValue();
639 if ( value > max )
640 {
641 max = value;
642 packageName = entry.getKey();
643 }
644 }
645
646 return packageName;
647 }
648
649 /**
650 * @param impl a Mojo implementation, not null
651 * @param project a MavenProject instance, could be null
652 * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>,
653 * <code>false</code> otherwise.
654 * @throws IllegalArgumentException if any
655 */
656 @SuppressWarnings( "unchecked" )
657 public static boolean isMavenReport( String impl, MavenProject project )
658 throws IllegalArgumentException
659 {
660 if ( impl == null )
661 {
662 throw new IllegalArgumentException( "mojo implementation should be declared" );
663 }
664
665 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
666 if ( project != null )
667 {
668 List<String> classPathStrings;
669 try
670 {
671 classPathStrings = project.getCompileClasspathElements();
672 if ( project.getExecutionProject() != null )
673 {
674 classPathStrings.addAll( project.getExecutionProject().getCompileClasspathElements() );
675 }
676 }
677 catch ( DependencyResolutionRequiredException e )
678 {
679 throw new IllegalArgumentException( e );
680 }
681
682 List<URL> urls = new ArrayList<URL>( classPathStrings.size() );
683 for ( String classPathString : classPathStrings )
684 {
685 try
686 {
687 urls.add( new File( classPathString ).toURL() );
688 }
689 catch ( MalformedURLException e )
690 {
691 throw new IllegalArgumentException( e );
692 }
693 }
694
695 classLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), classLoader );
696 }
697
698 try
699 {
700 Class<?> clazz = Class.forName( impl, false, classLoader );
701
702 return MavenReport.class.isAssignableFrom( clazz );
703 }
704 catch ( ClassNotFoundException e )
705 {
706 return false;
707 }
708 }
709
710 }