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 }