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