001package ball.tools.javadoc; 002/*- 003 * ########################################################################## 004 * Utilities 005 * $Id: AbstractTaglet.html 5431 2020-02-12 19:03:17Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/hcf-dev/blog/2019-03-30-java-interface-facades/src/main/resources/javadoc/src-html/ball/tools/javadoc/AbstractTaglet.html $ 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.xml.FluentDocument; 024import ball.xml.FluentDocumentBuilderFactory; 025import ball.xml.FluentNode; 026import com.sun.javadoc.ClassDoc; 027import com.sun.javadoc.ConstructorDoc; 028import com.sun.javadoc.Doc; 029import com.sun.javadoc.ExecutableMemberDoc; 030import com.sun.javadoc.FieldDoc; 031import com.sun.javadoc.MemberDoc; 032import com.sun.javadoc.MethodDoc; 033import com.sun.javadoc.PackageDoc; 034import com.sun.javadoc.ProgramElementDoc; 035import com.sun.javadoc.Tag; 036import com.sun.tools.doclets.internal.toolkit.Configuration; 037import com.sun.tools.doclets.internal.toolkit.util.DocLink; 038import java.beans.BeanInfo; 039import java.beans.Introspector; 040import java.io.StringWriter; 041import java.lang.reflect.Constructor; 042import java.lang.reflect.Executable; 043import java.lang.reflect.Field; 044import java.lang.reflect.Member; 045import java.lang.reflect.Method; 046import java.net.URI; 047import java.net.URL; 048import java.util.Arrays; 049import java.util.Collections; 050import java.util.Map; 051import java.util.regex.Pattern; 052import java.util.stream.Collectors; 053import javax.xml.transform.Transformer; 054import javax.xml.transform.TransformerFactory; 055import javax.xml.transform.dom.DOMSource; 056import javax.xml.transform.stream.StreamResult; 057import org.w3c.dom.Node; 058 059import static javax.xml.transform.OutputKeys.INDENT; 060import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION; 061import static org.apache.commons.lang3.StringUtils.EMPTY; 062import static org.apache.commons.lang3.StringUtils.countMatches; 063import static org.apache.commons.lang3.StringUtils.isNotEmpty; 064 065/** 066 * Abstract {@link com.sun.tools.doclets.Taglet} base class. 067 * 068 * <p>Note: {@link #getName()} implementation requires the subclass is 069 * annotated with {@link TagletName}.</p> 070 * 071 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 072 * @version $Revision: 5431 $ 073 */ 074public abstract class AbstractTaglet implements AnnotatedTaglet, 075 JavadocHTMLTemplates { 076 private static final String NO = "no"; 077 private static final String YES = "yes"; 078 079 private static final String INDENT_AMOUNT = 080 "{http://xml.apache.org/xslt}indent-amount"; 081 082 /** 083 * Implementation method for 084 * {@code public static void register(Map<Object,Object> map)}. 085 * 086 * @param map The {@link Map} to update. 087 * @param taglet The {@link AbstractTaglet} instance to 088 * register. 089 */ 090 protected static void register(Map<Object,Object> map, 091 AbstractTaglet taglet) { 092 map.putIfAbsent(taglet.getName(), taglet); 093 } 094 095 private final boolean isInlineTag; 096 private final boolean inPackage; 097 private final boolean inOverview; 098 private final boolean inField; 099 private final boolean inConstructor; 100 private final boolean inMethod; 101 private final boolean inType; 102 private final Transformer transformer; 103 private final FluentDocument document; 104 private Configuration configuration = null; 105 106 /** 107 * Sole constructor. 108 * 109 * @param isInlineTag See {@link #isInlineTag()}. 110 * @param inPackage See {@link #inPackage()}. 111 * @param inOverview See {@link #inOverview()}. 112 * @param inField See {@link #inField()}. 113 * @param inConstructor See {@link #inConstructor()}. 114 * @param inMethod See {@link #inMethod()}. 115 * @param inType See {@link #inType()}. 116 */ 117 protected AbstractTaglet(boolean isInlineTag, boolean inPackage, 118 boolean inOverview, boolean inField, 119 boolean inConstructor, boolean inMethod, 120 boolean inType) { 121 this.isInlineTag = isInlineTag; 122 this.inPackage = isInlineTag | inPackage; 123 this.inOverview = isInlineTag | inOverview; 124 this.inField = isInlineTag | inField; 125 this.inConstructor = isInlineTag | inConstructor; 126 this.inMethod = isInlineTag | inMethod; 127 this.inType = isInlineTag | inType; 128 129 try { 130 transformer = TransformerFactory.newInstance().newTransformer(); 131 transformer.setOutputProperty(OMIT_XML_DECLARATION, YES); 132 transformer.setOutputProperty(INDENT, NO); 133 134 document = 135 FluentDocumentBuilderFactory.newInstance() 136 .newDocumentBuilder() 137 .newDocument(); 138 document 139 .add(element("html", 140 element("head", 141 element("meta", 142 attr("charset", "utf-8"))), 143 element("body"))); 144 } catch (Exception exception) { 145 throw new ExceptionInInitializerError(exception); 146 } 147 } 148 149 @Override 150 public String getName() { 151 String name = AnnotatedTaglet.super.getName(); 152 153 if (name == null) { 154 name = getClass().getSimpleName().toLowerCase(); 155 } 156 157 return name; 158 } 159 160 @Override public boolean isInlineTag() { return isInlineTag; } 161 @Override public boolean inPackage() { return inPackage; } 162 @Override public boolean inOverview() { return inOverview; } 163 @Override public boolean inField() { return inField; } 164 @Override public boolean inConstructor() { return inConstructor; } 165 @Override public boolean inMethod() { return inMethod; } 166 @Override public boolean inType() { return inType; } 167 168 @Override 169 public FluentDocument document() { return document; } 170 171 @Override 172 public String toString(Tag[] tags) throws IllegalStateException { 173 throw new IllegalStateException(); 174 } 175 176 @Override 177 public String toString(Tag tag) throws IllegalStateException { 178 Node node = null; 179 180 try { 181 node = toNode(tag); 182 } catch (IllegalStateException exception) { 183 throw exception; 184 } catch (Error error) { 185 throw error; 186 } catch (Throwable throwable) { 187 node = warning(tag, throwable); 188 } 189 190 return render(node); 191 } 192 193 /** 194 * Implementation for {@link SunToolsInternalToolkitTaglet}. 195 * 196 * @param configuration The {@link Configuration}. 197 */ 198 public void set(Configuration configuration) { 199 this.configuration = configuration; 200 } 201 202 protected abstract Node toNode(Tag tag) throws Throwable; 203 204 /** 205 * Method to render a {@link Node} to a {@link String} without 206 * formatting or indentation. 207 * 208 * @param node The {@link Node}. 209 * 210 * @return The {@link String} representation. 211 * 212 * @throws RuntimeException 213 * Instead of checked {@link Exception}. 214 */ 215 protected String render(Node node) { return render(node, 0); } 216 217 /** 218 * Method to render a {@link Node} to a {@link String} with or without 219 * formatting or indentation. 220 * 221 * @param node The {@link Node}. 222 * @param indent The amount to indent; {@code <= 0} for no 223 * indentation. 224 * 225 * @return The {@link String} representation. 226 * 227 * @throws RuntimeException 228 * Instead of checked {@link Exception}. 229 */ 230 protected String render(Node node, int indent) { 231 StringWriter writer = new StringWriter(); 232 233 try { 234 transformer 235 .setOutputProperty(INDENT, (indent > 0) ? YES : NO); 236 transformer 237 .setOutputProperty(INDENT_AMOUNT, 238 String.valueOf(indent > 0 ? indent : 0)); 239 transformer 240 .transform(new DOMSource(node), 241 new StreamResult(writer)); 242 } catch (RuntimeException exception) { 243 throw exception; 244 } catch (Error error) { 245 throw error; 246 } catch (Exception exception) { 247 throw new RuntimeException(exception); 248 } 249 250 return writer.toString(); 251 } 252 253 /** 254 * Convenience method to attempt to find a {@link ClassDoc}. 255 * 256 * @param tag The {@link Tag}. 257 * @param type The {@link Class}. 258 * 259 * @return The {@link ClassDoc} if it can be found; {@code null} 260 * otherwise. 261 */ 262 protected ClassDoc getClassDocFor(Tag tag, Class<?> type) { 263 return getClassDocFor(tag.holder(), type.getCanonicalName()); 264 } 265 266 /** 267 * Convenience method to attempt to find a {@link ClassDoc}. 268 * 269 * @param tag The {@link Tag}. 270 * @param name The name to qualify. 271 * 272 * @return The {@link ClassDoc} if it can be found; {@code null} 273 * otherwise. 274 */ 275 protected ClassDoc getClassDocFor(Tag tag, String name) { 276 return getClassDocFor(tag.holder(), name); 277 } 278 279 private ClassDoc getClassDocFor(Doc context, String name) { 280 return getClassDocFor(getContainingClassDocFor(context), name); 281 } 282 283 private ClassDoc getClassDocFor(ClassDoc context, String name) { 284 return ((context != null) 285 ? (isNotEmpty(name) ? context.findClass(name) : context) 286 : null); 287 } 288 289 /** 290 * Convenience method to attempt to find a {@link FieldDoc}. 291 * 292 * @param tag The {@link Tag}. 293 * @param field The {@link Field}. 294 * 295 * @return The {@link FieldDoc} if it can be found; {@code null} 296 * otherwise. 297 */ 298 protected FieldDoc getFieldDocFor(Tag tag, Field field) { 299 FieldDoc fieldDoc = null; 300 ClassDoc classDoc = getClassDocFor(tag, field.getDeclaringClass()); 301 302 if (classDoc != null) { 303 fieldDoc = 304 Arrays.stream(classDoc.fields(true)) 305 .filter(t -> t.name().equals(field.getName())) 306 .findFirst().orElse(null); 307 } 308 309 return fieldDoc; 310 } 311 312 /** 313 * Convenience method to attempt to find a {@link ConstructorDoc}. 314 * 315 * @param tag The {@link Tag}. 316 * @param constructor The {@link Constructor}. 317 * 318 * @return The {@link ConstructorDoc} if it can be found; {@code null} 319 * otherwise. 320 */ 321 protected ConstructorDoc getConstructorDocFor(Tag tag, 322 Constructor<?> constructor) { 323 ConstructorDoc constructorDoc = null; 324 ClassDoc classDoc = 325 getClassDocFor(tag, constructor.getDeclaringClass()); 326 327 if (classDoc != null) { 328 constructorDoc = 329 Arrays.stream(classDoc.constructors(true)) 330 .filter(t -> t.signature().equals(signature(constructor))) 331 .findFirst().orElse(null); 332 } 333 334 return constructorDoc; 335 } 336 337 /** 338 * Convenience method to attempt to find a {@link MethodDoc}. 339 * 340 * @param tag The {@link Tag}. 341 * @param method The {@link Method}. 342 * 343 * @return The {@link MethodDoc} if it can be found; {@code null} 344 * otherwise. 345 */ 346 protected MethodDoc getMethodDocFor(Tag tag, Method method) { 347 MethodDoc methodDoc = null; 348 ClassDoc classDoc = getClassDocFor(tag, method.getDeclaringClass()); 349 350 if (classDoc != null) { 351 methodDoc = 352 Arrays.stream(classDoc.methods(true)) 353 .filter(t -> t.name().equals(method.getName())) 354 .filter(t -> t.signature().equals(signature(method))) 355 .findFirst().orElse(null); 356 } 357 358 return methodDoc; 359 } 360 361 private String signature(Executable executable) { 362 String signature = 363 Arrays.stream(executable.getParameterTypes()) 364 .map(t -> t.getCanonicalName()) 365 .collect(Collectors.joining(",", "(", ")")); 366 367 return signature; 368 } 369 370 /** 371 * Convenience method to get the containing {@link ClassDoc}. 372 * 373 * @param tag The {@link Tag}. 374 * 375 * @return The containing {@link ClassDoc} or {@code null} if there is 376 * none. 377 */ 378 protected ClassDoc getContainingClassDocFor(Tag tag) { 379 return getContainingClassDocFor(tag.holder()); 380 } 381 382 private ClassDoc getContainingClassDocFor(Doc doc) { 383 ClassDoc container = null; 384 385 if (doc instanceof ClassDoc) { 386 container = (ClassDoc) doc; 387 } else if (doc instanceof ProgramElementDoc) { 388 container = 389 getContainingClassDocFor(((ProgramElementDoc) doc) 390 .containingClass()); 391 } 392 393 return container; 394 } 395 396 /** 397 * Method to get the corresponding {@link Class} 398 * ({@code package-info.class}) for a {@link PackageDoc}. 399 * 400 * @param doc The {@link PackageDoc} (may be {@code null}). 401 * 402 * @return The corresponding {@link Class}. 403 * 404 * @throws RuntimeException 405 * Instead of checked {@link Exception}. 406 */ 407 protected Class<?> getClassFor(PackageDoc doc) { 408 Class<?> type = null; 409 410 try { 411 if (doc != null) { 412 String name = doc.name() + ".package-info"; 413 414 type = Class.forName(name); 415 } 416 } catch (RuntimeException exception) { 417 throw exception; 418 } catch (Error error) { 419 throw error; 420 } catch (Exception exception) { 421 throw new RuntimeException(exception); 422 } 423 424 return type; 425 } 426 427 /** 428 * Method to get the corresponding {@link Class} for a 429 * {@link ClassDoc}. 430 * 431 * @param doc The {@link ClassDoc} (may be {@code null}). 432 * 433 * @return The corresponding {@link Class}. 434 * 435 * @throws RuntimeException 436 * Instead of checked {@link Exception}. 437 */ 438 protected Class<?> getClassFor(ClassDoc doc) { 439 Class<?> type = null; 440 441 try { 442 if (doc != null) { 443 type = Class.forName(getClassNameFor(doc)); 444 } 445 } catch (RuntimeException exception) { 446 throw exception; 447 } catch (Error error) { 448 throw error; 449 } catch (Exception exception) { 450 throw new RuntimeException(exception); 451 } 452 453 return type; 454 } 455 456 private String getClassNameFor(ClassDoc doc) { 457 String name = null; 458 459 if (doc != null) { 460 name = 461 (doc.containingClass() != null) 462 ? (getClassNameFor(doc.containingClass()) 463 + "$" + doc.simpleTypeName()) 464 : doc.qualifiedName(); 465 } 466 467 return name; 468 } 469 470 /** 471 * Method to get a {@link Class}'s resource path. 472 * 473 * @param type The {@link Class}. 474 * 475 * @return The {@link Class}'s resource path (as a {@link String}). 476 */ 477 protected String getResourcePathOf(Class<?> type) { 478 String path = 479 String.join("/", type.getName().split(Pattern.quote("."))) 480 + ".class"; 481 482 return path; 483 } 484 485 /** 486 * Method to get the {@link URL} to a {@link Class}. 487 * 488 * @param type The {@link Class}. 489 * 490 * @return The {@link Class}'s {@link URL}. 491 */ 492 protected URL getResourceURLOf(Class<?> type) { 493 return type.getResource("/" + getResourcePathOf(type)); 494 } 495 496 /** 497 * See {@link Introspector#getBeanInfo(Class,Class)}. 498 * 499 * @param start The start {@link Class}. 500 * @param stop The stop {@link Class}. 501 * 502 * @return {@link BeanInfo} 503 * 504 * @throws RuntimeException 505 * Instead of checked {@link Exception}. 506 */ 507 protected BeanInfo getBeanInfo(Class<?> start, Class<?> stop) { 508 BeanInfo info = null; 509 510 try { 511 info = Introspector.getBeanInfo(start, stop); 512 } catch (RuntimeException exception) { 513 throw exception; 514 } catch (Error error) { 515 throw error; 516 } catch (Exception exception) { 517 throw new RuntimeException(exception); 518 } 519 520 return info; 521 } 522 523 /** 524 * See {@link #getBeanInfo(Class,Class)}. 525 * 526 * @param start The start {@link Class}. 527 * 528 * @return {@link BeanInfo} 529 * 530 * @throws RuntimeException 531 * Instead of checked {@link Exception}. 532 */ 533 protected BeanInfo getBeanInfo(Class<?> start) { 534 return getBeanInfo(start, Object.class); 535 } 536 537 private URI href(Tag tag, ProgramElementDoc target) { 538 return href(getContainingClassDocFor(tag.holder()), target); 539 } 540 541 private URI href(ClassDoc source, ProgramElementDoc target) { 542 URI href = null; 543 544 if (target != null) { 545 ClassDoc classDoc = getContainingClassDocFor(target); 546 PackageDoc packageDoc = classDoc.containingPackage(); 547 548 if (target.isIncluded()) { 549 String path = "./"; 550 int depth = countMatches(source.qualifiedName(), "."); 551 552 path += String.join(EMPTY, Collections.nCopies(depth, "../")); 553 554 if (isNotEmpty(packageDoc.name())) { 555 path += 556 String.join("/", 557 packageDoc.name().split(Pattern.quote("."))) 558 + "/"; 559 } 560 561 path += classDoc.name() + ".html"; 562 563 href = URI.create(path).normalize(); 564 } else { 565 if (configuration != null) { 566 DocLink link = 567 configuration.extern 568 .getExternalLink(packageDoc.name(), 569 null, classDoc.name() + ".html"); 570 /* 571 * Link might be null because the class cannot be 572 * loaded. 573 */ 574 if (link != null) { 575 href = URI.create(link.toString()); 576 } 577 } 578 } 579 } 580 581 if (href != null) { 582 if (target instanceof MemberDoc) { 583 String fragment = "#" + target.name(); 584 585 if (target instanceof ExecutableMemberDoc) { 586 fragment += 587 ((ExecutableMemberDoc) target).signature() 588 .replaceAll("[(),]", "-"); 589 } 590 591 href = href.resolve(fragment); 592 } 593 } 594 595 return href; 596 } 597 598 private URI href(Tag tag, Class<?> type) { 599 URI href = null; 600 Doc context = tag.holder(); 601 ClassDoc source = getContainingClassDocFor(context); 602 ClassDoc target = getClassDocFor(source, type.getCanonicalName()); 603 604 if (target != null) { 605 href = href(source, target); 606 } 607 608 return href; 609 } 610 611 private URI href(Tag tag, Member member) { 612 URI href = null; 613 ProgramElementDoc target = null; 614 615 if (member instanceof Field) { 616 target = getFieldDocFor(tag, (Field) member); 617 } else if (member instanceof Constructor) { 618 target = getConstructorDocFor(tag, (Constructor) member); 619 } else if (member instanceof Method) { 620 target = getMethodDocFor(tag, (Method) member); 621 } 622 623 if (target != null) { 624 href = href(tag, target); 625 } 626 627 return href; 628 } 629 630 /** 631 * {@code <a href="}{@link ClassDoc type}{@code ">}{@link Node node}{@code </a>} 632 * 633 * @param tag The {@link Tag}. 634 * @param type The target {@link Class}. 635 * @param node The child {@link Node} (may be 636 * {@code null}). 637 * 638 * @return {@link org.w3c.dom.Element} 639 */ 640 @Override 641 public FluentNode a(Tag tag, Class<?> type, Node node) { 642 String brackets = EMPTY; 643 644 while (type.isArray()) { 645 brackets = "[]" + brackets; 646 type = type.getComponentType(); 647 } 648 649 ClassDoc target = getClassDocFor(tag, type); 650 651 if (node == null) { 652 String name = 653 ((target != null) ? target.name() : type.getCanonicalName()); 654 655 node = code(name + brackets); 656 } 657 658 return a(tag, target, node); 659 } 660 661 /** 662 * {@code <a href="}{@link ClassDoc member}{@code ">}{@link Node node}{@code </a>} 663 * 664 * @param tag The {@link Tag}. 665 * @param member The target {@link Member}. 666 * @param node The child {@link Node} (may be 667 * {@code null}). 668 * 669 * @return {@link org.w3c.dom.Element} 670 */ 671 @Override 672 public FluentNode a(Tag tag, Member member, Node node) { 673 if (node == null) { 674 node = code(member.getName()); 675 } 676 677 return a(href(tag, member), node); 678 } 679 680 /** 681 * {@code <a href="}{@link ClassDoc type}{@code ">}{@link Node node}{@code </a>} 682 * 683 * @param tag The {@link Tag}. 684 * @param name The target {@link Class} name. 685 * @param node The child {@link Node} (may be 686 * {@code null}). 687 * 688 * @return {@link org.w3c.dom.Element} 689 */ 690 @Override 691 public FluentNode a(Tag tag, String name, Node node) { 692 ClassDoc target = getClassDocFor(tag, name); 693 694 if (node == null) { 695 node = code((target != null) ? target.name() : name); 696 } 697 698 return a(tag, target, node); 699 } 700 701 private FluentNode a(Tag tag, ClassDoc target, Node node) { 702 if (node == null) { 703 node = code(target.name()); 704 } 705 706 return a((target != null) ? href(tag, target) : null, node); 707 } 708}