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