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