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