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