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