001package ball.tools.javadoc; 002/*- 003 * ########################################################################## 004 * Utilities 005 * $Id: MavenTaglet.java 6038 2020-05-25 06:11:37Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-util/trunk/src/main/java/ball/tools/javadoc/MavenTaglet.java $ 007 * %% 008 * Copyright (C) 2008 - 2020 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import ball.annotation.ServiceProviderFor; 024import ball.util.PropertiesImpl; 025import ball.xml.FluentNode; 026import com.sun.javadoc.ClassDoc; 027import com.sun.javadoc.PackageDoc; 028import com.sun.javadoc.Tag; 029import com.sun.tools.doclets.Taglet; 030import java.io.File; 031import java.io.FileNotFoundException; 032import java.io.IOException; 033import java.io.InputStream; 034import java.lang.reflect.Field; 035import java.net.JarURLConnection; 036import java.net.URL; 037import java.nio.file.Files; 038import java.nio.file.Path; 039import java.nio.file.Paths; 040import java.util.Map; 041import java.util.jar.JarEntry; 042import java.util.jar.JarFile; 043import java.util.regex.Pattern; 044import java.util.stream.Stream; 045import java.util.zip.ZipEntry; 046import javax.xml.parsers.DocumentBuilderFactory; 047import javax.xml.xpath.XPath; 048import javax.xml.xpath.XPathExpression; 049import javax.xml.xpath.XPathFactory; 050import lombok.NoArgsConstructor; 051import lombok.ToString; 052import org.apache.commons.lang3.reflect.FieldUtils; 053import org.w3c.dom.Document; 054import org.w3c.dom.Node; 055import org.w3c.dom.NodeList; 056 057import static javax.xml.xpath.XPathConstants.NODE; 058import static javax.xml.xpath.XPathConstants.NODESET; 059import static lombok.AccessLevel.PROTECTED; 060import static org.apache.commons.lang3.StringUtils.EMPTY; 061import static org.apache.commons.lang3.StringUtils.defaultIfBlank; 062import static org.apache.commons.lang3.StringUtils.isNotEmpty; 063 064/** 065 * Abstract base class for inline {@link Taglet}s that load 066 * {@link.uri https://maven.apache.org/index.html Maven} artifacts. 067 * 068 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 069 * @version $Revision: 6038 $ 070 */ 071@NoArgsConstructor(access = PROTECTED) 072public abstract class MavenTaglet extends AbstractInlineTaglet 073 implements SunToolsInternalToolkitTaglet { 074 private static final XPath XPATH = XPathFactory.newInstance().newXPath(); 075 076 private static final String POM_XML = "pom.xml"; 077 private static final String DEPENDENCY = "dependency"; 078 private static final String GROUP_ID = "groupId"; 079 private static final String ARTIFACT_ID = "artifactId"; 080 private static final String VERSION = "version"; 081 082 protected XPathExpression compile(String format, Object... argv) { 083 XPathExpression expression = null; 084 085 try { 086 expression = XPATH.compile(String.format(format, argv)); 087 } catch (Exception exception) { 088 throw new IllegalStateException(exception); 089 } 090 091 return expression; 092 } 093 094 /** 095 * Method to locate the POM from a {@link Tag}. 096 * 097 * @param tag The {@link Tag}. 098 * 099 * @return The POM {@link File}. 100 * 101 * @throws Exception If the POM {@link File} cannot be found. 102 */ 103 protected File getPomFileFor(Tag tag) throws Exception { 104 File parent = tag.position().file().getParentFile(); 105 String name = defaultIfBlank(tag.text().trim(), POM_XML); 106 File file = new File(parent, name); 107 108 while (parent != null) { 109 file = new File(parent, name); 110 111 if (file.isFile()) { 112 break; 113 } else { 114 file = null; 115 } 116 117 parent = parent.getParentFile(); 118 } 119 120 if (! (file != null && file.isFile())) { 121 throw new FileNotFoundException(name); 122 } 123 124 return file; 125 } 126 127 /** 128 * Inline {@link Taglet} to provide a report of fields whose values are 129 * configured by the {@link.uri https://maven.apache.org/index.html Maven} 130 * {@link.uri https://maven.apache.org/plugin-developers/index.html Plugin} 131 * {@code plugin.xml}. 132 */ 133 @ServiceProviderFor({ Taglet.class }) 134 @TagletName("maven.plugin.fields") 135 @NoArgsConstructor @ToString 136 public static class PluginFields extends MavenTaglet { 137 private static final PluginFields INSTANCE = new PluginFields(); 138 139 public static void register(Map<Object,Object> map) { 140 register(map, INSTANCE); 141 } 142 143 private static final String PLUGIN_XML = "META-INF/maven/plugin.xml"; 144 145 @Override 146 public FluentNode toNode(Tag tag) throws Throwable { 147 ClassDoc doc = null; 148 String[] argv = tag.text().trim().split("[\\p{Space}]+", 2); 149 150 if (isNotEmpty(argv[0])) { 151 doc = getClassDocFor(tag, argv[0]); 152 } else { 153 doc = containingClass(tag); 154 } 155 156 Class<?> type = getClassFor(doc); 157 URL url = getResourceURLOf(type); 158 Protocol protocol = Protocol.of(url); 159 Document document = null; 160 161 switch (protocol) { 162 case FILE: 163 String root = 164 url.getPath() 165 .replaceAll(Pattern.quote(getResourcePathOf(type)), EMPTY); 166 167 document = 168 DocumentBuilderFactory.newInstance() 169 .newDocumentBuilder() 170 .parse(root + PLUGIN_XML); 171 break; 172 173 case JAR: 174 try (JarFile jar = protocol.getJarFile(url)) { 175 ZipEntry entry = jar.getEntry(PLUGIN_XML); 176 177 try (InputStream in = jar.getInputStream(entry)) { 178 document = 179 DocumentBuilderFactory.newInstance() 180 .newDocumentBuilder() 181 .parse(in); 182 } 183 } 184 break; 185 } 186 187 if (document == null) { 188 throw new IllegalStateException("Cannot find " + PLUGIN_XML); 189 } 190 191 Node mojo = 192 (Node) 193 compile("/plugin/mojos/mojo[implementation='%s']", 194 type.getCanonicalName()) 195 .evaluate(document, NODE); 196 197 return div(attr("class", "summary"), 198 h3("Maven Plugin Parameter Summary"), 199 table(tag, type, mojo, 200 asStream((NodeList) 201 compile("parameters/parameter") 202 .evaluate(mojo, NODESET)))); 203 } 204 205 private FluentNode table(Tag tag, Class<?> type, 206 Node mojo, Stream<Node> parameters) { 207 return table(thead(tr(th(EMPTY), th("Field"), 208 th("Default"), th("Property"), 209 th("Required"), th("Editable"), 210 th("Description"))), 211 tbody(parameters.map(t -> tr(tag, type, mojo, t)))); 212 } 213 214 private FluentNode tr(Tag tag, Class<?> type, 215 Node mojo, Node parameter) { 216 FluentNode tr = fragment(); 217 218 try { 219 String name = compile("name").evaluate(parameter); 220 Field field = FieldUtils.getField(type, name, true); 221 222 if (field != null) { 223 tr = 224 tr(td((! type.equals(field.getDeclaringClass())) 225 ? type(tag, field.getDeclaringClass()) 226 : text(EMPTY)), 227 td(declaration(tag, field)), 228 td(code(compile("configuration/%s/@default-value", name) 229 .evaluate(mojo))), 230 td(code(compile("configuration/%s", name) 231 .evaluate(mojo))), 232 td(code(compile("required").evaluate(parameter))), 233 td(code(compile("editable").evaluate(parameter))), 234 td(p(compile("description").evaluate(parameter)))); 235 } 236 } catch (RuntimeException exception) { 237 throw exception; 238 } catch (Exception exception) { 239 throw new IllegalStateException(exception); 240 } 241 242 return tr; 243 } 244 } 245 246 /** 247 * Inline {@link Taglet} to include generated 248 * {@link.uri https://maven.apache.org/index.html Maven} 249 * {@link.uri https://maven.apache.org/plugin-developers/index.html Plugin} 250 * help documentation. 251 */ 252 @ServiceProviderFor({ Taglet.class }) 253 @TagletName("maven.plugin.help") 254 @NoArgsConstructor @ToString 255 public static class PluginHelp extends MavenTaglet { 256 private static final PluginHelp INSTANCE = new PluginHelp(); 257 258 public static void register(Map<Object,Object> map) { 259 register(map, INSTANCE); 260 } 261 262 private static final String NAME = "plugin-help.xml"; 263 private static final Pattern PATTERN = 264 Pattern.compile("META-INF/maven/(?<g>[^/]+)/(?<a>[^/]+)/" 265 + Pattern.quote(NAME)); 266 267 @Override 268 public FluentNode toNode(Tag tag) throws Throwable { 269 Class<?> type = null; 270 271 if (tag.holder() instanceof PackageDoc) { 272 type = getClassFor((PackageDoc) tag.holder()); 273 } else { 274 type = getClassFor(containingClass(tag)); 275 } 276 277 URL url = getResourceURLOf(type); 278 Protocol protocol = Protocol.of(url); 279 Document document = null; 280 281 switch (protocol) { 282 case FILE: 283 Path root = 284 Paths.get(url.getPath() 285 .replaceAll(Pattern.quote(getResourcePathOf(type)), EMPTY)); 286 Path path = 287 Files.walk(root, Integer.MAX_VALUE) 288 .filter(Files::isRegularFile) 289 .filter(t -> PATTERN.matcher(root.relativize(t).toString()).matches()) 290 .findFirst().orElse(null); 291 292 document = 293 DocumentBuilderFactory.newInstance() 294 .newDocumentBuilder() 295 .parse(path.toFile()); 296 break; 297 298 case JAR: 299 try (JarFile jar = protocol.getJarFile(url)) { 300 JarEntry entry = 301 jar.stream() 302 .filter(t -> PATTERN.matcher(t.getName()).matches()) 303 .findFirst().orElse(null); 304 305 try (InputStream in = jar.getInputStream(entry)) { 306 document = 307 DocumentBuilderFactory.newInstance() 308 .newDocumentBuilder() 309 .parse(in); 310 } 311 } 312 break; 313 } 314 315 if (document == null) { 316 throw new IllegalStateException("Cannot find " + NAME); 317 } 318 319 return div(attr("class", "summary"), 320 h3(compile("/plugin/name").evaluate(document)), 321 p(compile("/plugin/description").evaluate(document)), 322 table(tag, 323 asStream((NodeList) 324 compile("/plugin/mojos/mojo") 325 .evaluate(document, NODESET)))); 326 } 327 328 private FluentNode table(Tag tag, Stream<Node> mojos) { 329 return table(thead(tr(th("Goal"), th("Phase"), th("Description"))), 330 tbody(mojos.map(t -> tr(tag, t)))); 331 } 332 333 private FluentNode tr(Tag tag, Node mojo) { 334 FluentNode tr = fragment(); 335 336 try { 337 tr = 338 tr(td(a(tag, 339 compile("implementation").evaluate(mojo), 340 code(compile("goal").evaluate(mojo)))), 341 td(code(compile("phase").evaluate(mojo))), 342 td(p(code(compile("description").evaluate(mojo))))); 343 } catch (RuntimeException exception) { 344 throw exception; 345 } catch (Exception exception) { 346 throw new IllegalStateException(exception); 347 } 348 349 return tr; 350 } 351 } 352 353 /** 354 * {@link Taglet} to provide 355 * {@link.uri https://maven.apache.org/pom.html POM} POM coordinates as 356 * a {@code <dependency/>} element to include this documented 357 * {@link Class} or {@link Package}. 358 * 359 * <p>For example:</p> 360 * 361 * {@pom.coordinates} 362 */ 363 @ServiceProviderFor({ Taglet.class }) 364 @TagletName("pom.coordinates") 365 @NoArgsConstructor @ToString 366 public static class Coordinates extends MavenTaglet { 367 private static final Coordinates INSTANCE = new Coordinates(); 368 369 public static void register(Map<Object,Object> map) { 370 register(map, INSTANCE); 371 } 372 373 private static final Pattern PATTERN = 374 Pattern.compile("META-INF/maven/(?<g>[^/]+)/(?<a>[^/]+)/pom[.]properties"); 375 376 @Override 377 public FluentNode toNode(Tag tag) throws Throwable { 378 POMProperties properties = new POMProperties(); 379 Class<?> type = null; 380 381 if (tag.holder() instanceof PackageDoc) { 382 type = getClassFor((PackageDoc) tag.holder()); 383 } else { 384 type = getClassFor(containingClass(tag)); 385 } 386 387 URL url = getResourceURLOf(type); 388 Protocol protocol = Protocol.of(url); 389 390 switch (protocol) { 391 case FILE: 392 Document document = 393 DocumentBuilderFactory.newInstance() 394 .newDocumentBuilder() 395 .parse(getPomFileFor(tag)); 396 397 Stream.of(GROUP_ID, ARTIFACT_ID, VERSION) 398 .forEach(t -> properties.load(t, document, "/project/")); 399 Stream.of(VERSION) 400 .forEach(t -> properties.load(t, document, "/project/parent/")); 401 break; 402 403 case JAR: 404 try (JarFile jar = protocol.getJarFile(url)) { 405 JarEntry entry = 406 jar.stream() 407 .filter(t -> PATTERN.matcher(t.getName()).matches()) 408 .findFirst().orElse(null); 409 410 if (entry != null) { 411 try (InputStream in = jar.getInputStream(entry)) { 412 properties.load(in); 413 } 414 } 415 } 416 break; 417 } 418 419 return pre("xml", 420 render(element(DEPENDENCY, 421 Stream.of(GROUP_ID, ARTIFACT_ID, VERSION) 422 .map(t -> element(t).content(properties.getProperty(t, "unknown")))), 423 2)); 424 } 425 } 426 427 private static enum Protocol { 428 FILE, JAR; 429 430 public static Protocol of(URL url) { 431 return valueOf(url.getProtocol().toUpperCase()); 432 } 433 434 public JarFile getJarFile(URL url) throws IOException { 435 JarFile jar = null; 436 437 switch (this) { 438 case JAR: 439 jar = ((JarURLConnection) url.openConnection()).getJarFile(); 440 break; 441 442 default: 443 throw new IllegalStateException(); 444 /* break; */ 445 } 446 447 return jar; 448 } 449 } 450 451 private static class POMProperties extends PropertiesImpl { 452 private static final long serialVersionUID = 1354153174028868013L; 453 454 public String load(String key, Document document, String prefix) { 455 if (document != null) { 456 computeIfAbsent(key, k -> evaluate(prefix + k, document)); 457 } 458 459 return super.getProperty(key); 460 } 461 462 private String evaluate(String expression, Document document) { 463 String value = null; 464 465 try { 466 value = XPATH.evaluate(expression, document); 467 } catch (Exception exception) { 468 } 469 470 return isNotEmpty(value) ? value : null; 471 } 472 } 473}