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.net.MalformedURLException; 033import java.net.URL; 034import java.net.URLClassLoader; 035import java.nio.charset.StandardCharsets; 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 057/** 058 * Convenience methods to play with Maven plugins. 059 * 060 * @author jdcasey 061 */ 062public 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 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 /** 116 * @param artifacts not null collection of <code>Artifact</code> 117 * @return list of component dependencies, without in provided scope 118 */ 119 public static List<ComponentDependency> toComponentDependencies( Collection<Artifact> artifacts ) 120 { 121 List<ComponentDependency> componentDeps = new LinkedList<>(); 122 123 for ( Artifact artifact : artifacts ) 124 { 125 if ( Artifact.SCOPE_PROVIDED.equals( artifact.getScope() ) ) 126 { 127 continue; 128 } 129 130 ComponentDependency cd = new ComponentDependency(); 131 132 cd.setArtifactId( artifact.getArtifactId() ); 133 cd.setGroupId( artifact.getGroupId() ); 134 cd.setVersion( artifact.getVersion() ); 135 cd.setType( artifact.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 * @deprecated Only used for non java extractor 192 */ 193 @Deprecated 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 * @deprecated Redundant for java extractor 265 */ 266 @Deprecated 267 public static String makeHtmlValid( String description ) 268 { 269 270 if ( StringUtils.isEmpty( description ) ) 271 { 272 return ""; 273 } 274 275 String commentCleaned = decodeJavadocTags( description ); 276 277 // Using jTidy to clean comment 278 Tidy tidy = new Tidy(); 279 tidy.setDocType( "loose" ); 280 tidy.setXHTML( true ); 281 tidy.setXmlOut( true ); 282 tidy.setInputEncoding( "UTF-8" ); 283 tidy.setOutputEncoding( "UTF-8" ); 284 tidy.setMakeClean( true ); 285 tidy.setNumEntities( true ); 286 tidy.setQuoteNbsp( false ); 287 tidy.setQuiet( true ); 288 tidy.setShowWarnings( true ); 289 290 ByteArrayOutputStream out = new ByteArrayOutputStream( commentCleaned.length() + 256 ); 291 tidy.parse( new ByteArrayInputStream( commentCleaned.getBytes( StandardCharsets.UTF_8 ) ), out ); 292 commentCleaned = new String( out.toByteArray(), StandardCharsets.UTF_8 ); 293 294 if ( StringUtils.isEmpty( commentCleaned ) ) 295 { 296 return ""; 297 } 298 299 // strip the header/body stuff 300 String ls = System.getProperty( "line.separator" ); 301 int startPos = commentCleaned.indexOf( "<body>" + ls ) + 6 + ls.length(); 302 int endPos = commentCleaned.indexOf( ls + "</body>" ); 303 commentCleaned = commentCleaned.substring( startPos, endPos ); 304 305 return commentCleaned; 306 } 307 308 /** 309 * Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain 310 * as much of the text formatting as possible by means of the following transformations: 311 * <ul> 312 * <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and 313 * finally the item contents. Each tab denotes an increase of indentation.</li> 314 * <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline 315 * (U+000A) to denote a mandatory line break.</li> 316 * <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized 317 * to a single space. The resulting space denotes a possible point for line wrapping.</li> 318 * <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li> 319 * </ul> 320 * 321 * @param html The HTML fragment to convert to plain text, may be <code>null</code>. 322 * @return A string with HTML tags converted into pure text, never <code>null</code>. 323 * @since 2.4.3 324 * @deprecated Replaced by {@link HtmlToPlainTextConverter} 325 */ 326 @Deprecated 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 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<>(); 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 MojoParserCallback( StringBuilder sb ) 407 { 408 this.sb = sb; 409 } 410 411 /** {@inheritDoc} */ 412 @Override 413 public void handleSimpleTag( HTML.Tag t, MutableAttributeSet a, int pos ) 414 { 415 simpleTag = true; 416 if ( body && HTML.Tag.BR.equals( t ) ) 417 { 418 newline( false ); 419 } 420 } 421 422 /** {@inheritDoc} */ 423 @Override 424 public void handleStartTag( HTML.Tag t, MutableAttributeSet a, int pos ) 425 { 426 simpleTag = false; 427 if ( body && ( t.breaksFlow() || t.isBlock() ) ) 428 { 429 newline( true ); 430 } 431 if ( HTML.Tag.OL.equals( t ) ) 432 { 433 numbering.push( new Counter() ); 434 } 435 else if ( HTML.Tag.UL.equals( t ) ) 436 { 437 numbering.push( null ); 438 } 439 else if ( HTML.Tag.LI.equals( t ) ) 440 { 441 Counter counter = numbering.peek(); 442 if ( counter == null ) 443 { 444 text( "-\t" ); 445 } 446 else 447 { 448 text( ++counter.value + ".\t" ); 449 } 450 depth++; 451 } 452 else if ( HTML.Tag.DD.equals( t ) ) 453 { 454 depth++; 455 } 456 else if ( t.isPreformatted() ) 457 { 458 preformatted++; 459 } 460 else if ( HTML.Tag.BODY.equals( t ) ) 461 { 462 body = true; 463 } 464 } 465 466 /** {@inheritDoc} */ 467 @Override 468 public void handleEndTag( HTML.Tag t, int pos ) 469 { 470 if ( HTML.Tag.OL.equals( t ) || HTML.Tag.UL.equals( t ) ) 471 { 472 numbering.pop(); 473 } 474 else if ( HTML.Tag.LI.equals( t ) || HTML.Tag.DD.equals( t ) ) 475 { 476 depth--; 477 } 478 else if ( t.isPreformatted() ) 479 { 480 preformatted--; 481 } 482 else if ( HTML.Tag.BODY.equals( t ) ) 483 { 484 body = false; 485 } 486 if ( body && ( t.breaksFlow() || t.isBlock() ) && !HTML.Tag.LI.equals( t ) ) 487 { 488 if ( ( HTML.Tag.P.equals( t ) || HTML.Tag.PRE.equals( t ) || HTML.Tag.OL.equals( t ) 489 || HTML.Tag.UL.equals( t ) || HTML.Tag.DL.equals( t ) ) 490 && numbering.isEmpty() ) 491 { 492 pendingNewline = false; 493 newline( pendingNewline ); 494 } 495 else 496 { 497 newline( true ); 498 } 499 } 500 } 501 502 /** {@inheritDoc} */ 503 @Override 504 public void handleText( char[] data, int pos ) 505 { 506 /* 507 * NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by 508 * the text event ">..." so we need to watch out for the closing angle bracket. 509 */ 510 int offset = 0; 511 if ( simpleTag && data[0] == '>' ) 512 { 513 simpleTag = false; 514 for ( ++offset; offset < data.length && data[offset] <= ' '; ) 515 { 516 offset++; 517 } 518 } 519 if ( offset < data.length ) 520 { 521 String text = new String( data, offset, data.length - offset ); 522 text( text ); 523 } 524 } 525 526 /** {@inheritDoc} */ 527 @Override 528 public void flush() 529 { 530 flushPendingNewline(); 531 } 532 533 /** 534 * Writes a line break to the plain text output. 535 * 536 * @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are 537 * always written to the output whereas consecutive implicit line breaks are merged into a single 538 * line break. 539 */ 540 private void newline( boolean implicit ) 541 { 542 if ( implicit ) 543 { 544 pendingNewline = true; 545 } 546 else 547 { 548 flushPendingNewline(); 549 sb.append( '\n' ); 550 } 551 } 552 553 /** 554 * Flushes a pending newline (if any). 555 */ 556 private void flushPendingNewline() 557 { 558 if ( pendingNewline ) 559 { 560 pendingNewline = false; 561 if ( sb.length() > 0 ) 562 { 563 sb.append( '\n' ); 564 } 565 } 566 } 567 568 /** 569 * Writes the specified character data to the plain text output. If the last output was a line break, the 570 * character data will automatically be prefixed with the current indent. 571 * 572 * @param data The character data, must not be <code>null</code>. 573 */ 574 private void text( String data ) 575 { 576 flushPendingNewline(); 577 if ( sb.length() <= 0 || sb.charAt( sb.length() - 1 ) == '\n' ) 578 { 579 for ( int i = 0; i < depth; i++ ) 580 { 581 sb.append( '\t' ); 582 } 583 } 584 String text; 585 if ( preformatted > 0 ) 586 { 587 text = data; 588 } 589 else 590 { 591 text = data.replace( '\n', ' ' ); 592 } 593 sb.append( text ); 594 } 595 } 596 597 /** 598 * Find the best package name, based on the number of hits of actual Mojo classes. 599 * 600 * @param pluginDescriptor not null 601 * @return the best name of the package for the generated mojo 602 */ 603 public static String discoverPackageName( PluginDescriptor pluginDescriptor ) 604 { 605 Map<String, Integer> packageNames = new HashMap<>(); 606 607 List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos(); 608 if ( mojoDescriptors == null ) 609 { 610 return ""; 611 } 612 for ( MojoDescriptor descriptor : mojoDescriptors ) 613 { 614 615 String impl = descriptor.getImplementation(); 616 if ( StringUtils.equals( descriptor.getGoal(), "help" ) && StringUtils.equals( "HelpMojo", impl ) ) 617 { 618 continue; 619 } 620 if ( impl.lastIndexOf( '.' ) != -1 ) 621 { 622 String name = impl.substring( 0, impl.lastIndexOf( '.' ) ); 623 if ( packageNames.get( name ) != null ) 624 { 625 int next = ( packageNames.get( name ) ).intValue() + 1; 626 packageNames.put( name, Integer.valueOf( next ) ); 627 } 628 else 629 { 630 packageNames.put( name, Integer.valueOf( 1 ) ); 631 } 632 } 633 else 634 { 635 packageNames.put( "", Integer.valueOf( 1 ) ); 636 } 637 } 638 639 String packageName = ""; 640 int max = 0; 641 for ( Map.Entry<String, Integer> entry : packageNames.entrySet() ) 642 { 643 int value = entry.getValue().intValue(); 644 if ( value > max ) 645 { 646 max = value; 647 packageName = entry.getKey(); 648 } 649 } 650 651 return packageName; 652 } 653 654 /** 655 * @param impl a Mojo implementation, not null 656 * @param project a MavenProject instance, could be null 657 * @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>, 658 * <code>false</code> otherwise. 659 * @throws IllegalArgumentException if any 660 */ 661 @SuppressWarnings( "unchecked" ) 662 public static boolean isMavenReport( String impl, MavenProject project ) 663 throws IllegalArgumentException 664 { 665 if ( impl == null ) 666 { 667 throw new IllegalArgumentException( "mojo implementation should be declared" ); 668 } 669 670 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 671 if ( project != null ) 672 { 673 List<String> classPathStrings; 674 try 675 { 676 classPathStrings = project.getCompileClasspathElements(); 677 if ( project.getExecutionProject() != null ) 678 { 679 classPathStrings.addAll( project.getExecutionProject().getCompileClasspathElements() ); 680 } 681 } 682 catch ( DependencyResolutionRequiredException e ) 683 { 684 throw new IllegalArgumentException( e ); 685 } 686 687 List<URL> urls = new ArrayList<>( classPathStrings.size() ); 688 for ( String classPathString : classPathStrings ) 689 { 690 try 691 { 692 urls.add( new File( classPathString ).toURL() ); 693 } 694 catch ( MalformedURLException e ) 695 { 696 throw new IllegalArgumentException( e ); 697 } 698 } 699 700 classLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), classLoader ); 701 } 702 703 try 704 { 705 Class<?> clazz = Class.forName( impl, false, classLoader ); 706 707 return MavenReport.class.isAssignableFrom( clazz ); 708 } 709 catch ( ClassNotFoundException e ) 710 { 711 return false; 712 } 713 } 714 715}