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