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