001package ball.tools.javadoc; 002/*- 003 * ########################################################################## 004 * Utilities 005 * $Id: AbstractTaglet.java 6202 2020-06-16 13:44:11Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-util/trunk/src/main/java/ball/tools/javadoc/AbstractTaglet.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.xml.FluentDocument; 024import ball.xml.FluentDocumentBuilderFactory; 025import ball.xml.FluentNode; 026import ball.xml.XalanConstants; 027import com.sun.javadoc.ClassDoc; 028import com.sun.javadoc.ConstructorDoc; 029import com.sun.javadoc.Doc; 030import com.sun.javadoc.ExecutableMemberDoc; 031import com.sun.javadoc.FieldDoc; 032import com.sun.javadoc.MemberDoc; 033import com.sun.javadoc.MethodDoc; 034import com.sun.javadoc.PackageDoc; 035import com.sun.javadoc.ProgramElementDoc; 036import com.sun.javadoc.Tag; 037import com.sun.tools.doclets.internal.toolkit.Configuration; 038import com.sun.tools.doclets.internal.toolkit.util.DocLink; 039import java.beans.BeanInfo; 040import java.beans.Introspector; 041import java.io.StringWriter; 042import java.lang.reflect.Constructor; 043import java.lang.reflect.Executable; 044import java.lang.reflect.Field; 045import java.lang.reflect.Member; 046import java.lang.reflect.Method; 047import java.net.URI; 048import java.net.URL; 049import java.util.Arrays; 050import java.util.Collections; 051import java.util.Map; 052import java.util.Objects; 053import java.util.regex.Pattern; 054import java.util.stream.Collectors; 055import java.util.stream.Stream; 056import javax.xml.transform.Transformer; 057import javax.xml.transform.TransformerFactory; 058import javax.xml.transform.dom.DOMSource; 059import javax.xml.transform.stream.StreamResult; 060import org.w3c.dom.Node; 061 062import static javax.xml.transform.OutputKeys.INDENT; 063import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION; 064import static org.apache.commons.lang3.StringUtils.EMPTY; 065import static org.apache.commons.lang3.StringUtils.countMatches; 066import static org.apache.commons.lang3.StringUtils.isNotEmpty; 067 068/** 069 * Abstract {@link com.sun.tools.doclets.Taglet} base class. 070 * See {@link #toNode(Tag)}. 071 * 072 * <p>Note: {@link #getName()} implementation requires the subclass is 073 * annotated with {@link TagletName}.</p> 074 * 075 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 076 * @version $Revision: 6202 $ 077 */ 078public abstract class AbstractTaglet implements AnnotatedTaglet, 079 JavadocHTMLTemplates, 080 XalanConstants { 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 /** 203 * Abstract method to be overridden by subclass implementations. 204 * 205 * @param tag The {@link Tag}. 206 * 207 * @return The {@link Node} representing the output. 208 * 209 * @throws Throwable If the method fails for any reason. 210 */ 211 protected abstract Node toNode(Tag tag) throws Throwable; 212 213 /** 214 * Method to render a {@link Node} to a {@link String} without 215 * formatting or indentation. 216 * 217 * @param node The {@link Node}. 218 * 219 * @return The {@link String} representation. 220 * 221 * @throws RuntimeException 222 * Instead of checked {@link Exception}. 223 */ 224 protected String render(Node node) { return render(node, 0); } 225 226 /** 227 * Method to render a {@link Node} to a {@link String} with or without 228 * formatting or indentation. 229 * 230 * @param node The {@link Node}. 231 * @param indent The amount to indent; {@code <= 0} for no 232 * indentation. 233 * 234 * @return The {@link String} representation. 235 * 236 * @throws RuntimeException 237 * Instead of checked {@link Exception}. 238 */ 239 protected String render(Node node, int indent) { 240 StringWriter writer = new StringWriter(); 241 242 try { 243 transformer 244 .setOutputProperty(INDENT, (indent > 0) ? YES : NO); 245 transformer 246 .setOutputProperty(XALAN_INDENT_AMOUNT.toString(), 247 String.valueOf(indent > 0 ? indent : 0)); 248 transformer 249 .transform(new DOMSource(node), 250 new StreamResult(writer)); 251 } catch (RuntimeException exception) { 252 throw exception; 253 } catch (Error error) { 254 throw error; 255 } catch (Exception exception) { 256 throw new RuntimeException(exception); 257 } 258 259 return writer.toString(); 260 } 261 262 /** 263 * Method to get the containing {@link ClassDoc}. See 264 * {@link ProgramElementDoc#containingClass()}. 265 * 266 * @param tag The {@link Tag}. 267 * 268 * @return The containing {@link ClassDoc} (may be {@code null}). 269 */ 270 protected ClassDoc containingClass(Tag tag) { 271 return containingClass(tag.holder()); 272 } 273 274 private ClassDoc containingClass(Doc holder) { 275 ClassDoc doc = null; 276 277 if (holder instanceof ClassDoc) { 278 doc = (ClassDoc) holder; 279 } else if (holder instanceof ProgramElementDoc) { 280 doc = ((ProgramElementDoc) holder).containingClass(); 281 } 282 283 return doc; 284 } 285 286 /** 287 * Method to get the containing {@link PackageDoc}. See 288 * {@link ProgramElementDoc#containingPackage()}. 289 * 290 * @param tag The {@link Tag}. 291 * 292 * @return The containing {@link PackageDoc} (may be {@code null}). 293 */ 294 protected PackageDoc containingPackage(Tag tag) { 295 return containingPackage(tag.holder()); 296 } 297 298 private PackageDoc containingPackage(Doc holder) { 299 PackageDoc doc = null; 300 301 if (holder instanceof PackageDoc) { 302 doc = (PackageDoc) holder; 303 } else if (holder instanceof ProgramElementDoc) { 304 doc = ((ProgramElementDoc) holder).containingPackage(); 305 } 306 307 return doc; 308 } 309 310 /** 311 * Method to attempt to find a {@link ClassDoc}. 312 * 313 * @param tag The {@link Tag}. 314 * @param name The {@link Class} name. 315 * 316 * @return The {@link ClassDoc} if it can be found; {@code null} 317 * otherwise. 318 */ 319 protected ClassDoc getClassDocFor(Tag tag, String name) { 320 return findClass(tag.holder(), name); 321 } 322 323 private ClassDoc findClass(Doc holder, String name) { 324 ClassDoc doc = null; 325 326 if (holder instanceof ClassDoc) { 327 doc = findClass((ClassDoc) holder, name); 328 } else if (holder instanceof PackageDoc) { 329 doc = findClass((PackageDoc) holder, name); 330 } else if (holder instanceof MemberDoc) { 331 doc = findClass(((MemberDoc) holder).containingClass(), name); 332 } 333 334 return doc; 335 } 336 337 private ClassDoc findClass(ClassDoc holder, String name) { 338 return holder.findClass(name); 339 } 340 341 private ClassDoc findClass(PackageDoc holder, String name) { 342 ClassDoc doc = 343 Stream.of(holder.allClasses(true)) 344 .map(t -> t.findClass(name)) 345 .filter(Objects::nonNull) 346 .findFirst().orElse(holder.findClass(name)); 347 348 return doc; 349 } 350 351 /** 352 * Method to attempt to find a {@link ClassDoc}. 353 * 354 * @param tag The {@link Tag}. 355 * @param type The {@link Class}. 356 * 357 * @return The {@link ClassDoc} if it can be found; {@code null} 358 * otherwise. 359 */ 360 protected ClassDoc getClassDocFor(Tag tag, Class<?> type) { 361 return getClassDocFor(tag, type.getCanonicalName()); 362 } 363 364 /** 365 * Method to attempt to find a {@link FieldDoc}. 366 * 367 * @param tag The {@link Tag}. 368 * @param field The {@link Field}. 369 * 370 * @return The {@link FieldDoc} if it can be found; {@code null} 371 * otherwise. 372 */ 373 protected FieldDoc getFieldDocFor(Tag tag, Field field) { 374 FieldDoc fieldDoc = null; 375 ClassDoc classDoc = getClassDocFor(tag, field.getDeclaringClass()); 376 377 if (classDoc != null) { 378 fieldDoc = 379 Arrays.stream(classDoc.fields(true)) 380 .filter(t -> t.name().equals(field.getName())) 381 .findFirst().orElse(null); 382 } 383 384 return fieldDoc; 385 } 386 387 /** 388 * Method to attempt to find a {@link ConstructorDoc}. 389 * 390 * @param tag The {@link Tag}. 391 * @param constructor The {@link Constructor}. 392 * 393 * @return The {@link ConstructorDoc} if it can be found; {@code null} 394 * otherwise. 395 */ 396 protected ConstructorDoc getConstructorDocFor(Tag tag, 397 Constructor<?> constructor) { 398 ConstructorDoc constructorDoc = null; 399 ClassDoc classDoc = 400 getClassDocFor(tag, constructor.getDeclaringClass()); 401 402 if (classDoc != null) { 403 constructorDoc = 404 Arrays.stream(classDoc.constructors(true)) 405 .filter(t -> t.signature().equals(signature(constructor))) 406 .findFirst().orElse(null); 407 } 408 409 return constructorDoc; 410 } 411 412 /** 413 * Method to attempt to find a {@link MethodDoc}. 414 * 415 * @param tag The {@link Tag}. 416 * @param method The {@link Method}. 417 * 418 * @return The {@link MethodDoc} if it can be found; {@code null} 419 * otherwise. 420 */ 421 protected MethodDoc getMethodDocFor(Tag tag, Method method) { 422 MethodDoc methodDoc = null; 423 ClassDoc classDoc = getClassDocFor(tag, method.getDeclaringClass()); 424 425 if (classDoc != null) { 426 methodDoc = 427 Arrays.stream(classDoc.methods(true)) 428 .filter(t -> t.name().equals(method.getName())) 429 .filter(t -> t.signature().equals(signature(method))) 430 .findFirst().orElse(null); 431 } 432 433 return methodDoc; 434 } 435 436 private String signature(Executable executable) { 437 String signature = 438 Arrays.stream(executable.getParameterTypes()) 439 .map(t -> t.getCanonicalName()) 440 .collect(Collectors.joining(",", "(", ")")); 441 442 return signature; 443 } 444 445 /** 446 * Method to get the corresponding {@link Class} 447 * ({@code package-info.class}) for a {@link PackageDoc}. 448 * 449 * @param doc The {@link PackageDoc} (may be {@code null}). 450 * 451 * @return The corresponding {@link Class}. 452 * 453 * @throws RuntimeException 454 * Instead of checked {@link Exception}. 455 */ 456 protected Class<?> getClassFor(PackageDoc doc) { 457 Class<?> type = null; 458 459 try { 460 if (doc != null) { 461 String name = doc.name() + ".package-info"; 462 463 type = Class.forName(name); 464 } 465 } catch (RuntimeException exception) { 466 throw exception; 467 } catch (Error error) { 468 throw error; 469 } catch (Exception exception) { 470 throw new RuntimeException(exception); 471 } 472 473 return type; 474 } 475 476 /** 477 * Method to get the corresponding {@link Class} for a 478 * {@link ClassDoc}. 479 * 480 * @param doc The {@link ClassDoc} (may be {@code null}). 481 * 482 * @return The corresponding {@link Class}. 483 * 484 * @throws RuntimeException 485 * Instead of checked {@link Exception}. 486 */ 487 protected Class<?> getClassFor(ClassDoc doc) { 488 Class<?> type = null; 489 490 try { 491 if (doc != null) { 492 type = Class.forName(getClassNameFor(doc)); 493 } 494 } catch (RuntimeException exception) { 495 throw exception; 496 } catch (Error error) { 497 throw error; 498 } catch (Exception exception) { 499 throw new RuntimeException(exception); 500 } 501 502 return type; 503 } 504 505 private String getClassNameFor(ClassDoc doc) { 506 String name = null; 507 508 if (doc != null) { 509 name = 510 (doc.containingClass() != null) 511 ? (getClassNameFor(doc.containingClass()) 512 + "$" + doc.simpleTypeName()) 513 : doc.qualifiedName(); 514 } 515 516 return name; 517 } 518 519 /** 520 * Method to get a {@link Class}'s resource path. 521 * 522 * @param type The {@link Class}. 523 * 524 * @return The {@link Class}'s resource path (as a {@link String}). 525 */ 526 protected String getResourcePathOf(Class<?> type) { 527 String path = 528 String.join("/", type.getName().split(Pattern.quote("."))) 529 + ".class"; 530 531 return path; 532 } 533 534 /** 535 * Method to get the {@link URL} to a {@link Class}. 536 * 537 * @param type The {@link Class}. 538 * 539 * @return The {@link Class}'s {@link URL}. 540 */ 541 protected URL getResourceURLOf(Class<?> type) { 542 return type.getResource("/" + getResourcePathOf(type)); 543 } 544 545 /** 546 * See {@link Introspector#getBeanInfo(Class,Class)}. 547 * 548 * @param start The start {@link Class}. 549 * @param stop The stop {@link Class}. 550 * 551 * @return {@link BeanInfo} 552 * 553 * @throws RuntimeException 554 * Instead of checked {@link Exception}. 555 */ 556 protected BeanInfo getBeanInfo(Class<?> start, Class<?> stop) { 557 BeanInfo info = null; 558 559 try { 560 info = Introspector.getBeanInfo(start, stop); 561 } catch (RuntimeException exception) { 562 throw exception; 563 } catch (Error error) { 564 throw error; 565 } catch (Exception exception) { 566 throw new RuntimeException(exception); 567 } 568 569 return info; 570 } 571 572 /** 573 * See {@link #getBeanInfo(Class,Class)}. 574 * 575 * @param start The start {@link Class}. 576 * 577 * @return {@link BeanInfo} 578 * 579 * @throws RuntimeException 580 * Instead of checked {@link Exception}. 581 */ 582 protected BeanInfo getBeanInfo(Class<?> start) { 583 return getBeanInfo(start, Object.class); 584 } 585 586 /** 587 * {@code <a href="}{@link ClassDoc type}{@code ">}{@link Node node}{@code </a>} 588 * 589 * @param tag The {@link Tag}. 590 * @param target The target {@link ClassDoc}. 591 * @param node The child {@link Node} (may be 592 * {@code null}). 593 * 594 * @return {@link org.w3c.dom.Element} 595 */ 596 @Override 597 public FluentNode a(Tag tag, ProgramElementDoc target, Node node) { 598 if (node == null) { 599 node = code(target.name()); 600 } 601 602 return a((target != null) ? href(tag, target) : null, node); 603 } 604 605 /** 606 * {@code <a href="}{@link ClassDoc type}{@code ">}{@link Node node}{@code </a>} 607 * 608 * @param tag The {@link Tag}. 609 * @param type The target {@link Class}. 610 * @param node The child {@link Node} (may be 611 * {@code null}). 612 * 613 * @return {@link org.w3c.dom.Element} 614 */ 615 @Override 616 public FluentNode a(Tag tag, Class<?> type, Node node) { 617 String brackets = EMPTY; 618 619 while (type.isArray()) { 620 brackets = "[]" + brackets; 621 type = type.getComponentType(); 622 } 623 624 ClassDoc target = getClassDocFor(tag, type); 625 626 if (node == null) { 627 String name = 628 ((target != null) ? target.name() : type.getCanonicalName()); 629 630 node = code(name + brackets); 631 } 632 633 return a(tag, target, node); 634 } 635 636 /** 637 * {@code <a href="}{@link ClassDoc member}{@code ">}{@link Node node}{@code </a>} 638 * 639 * @param tag The {@link Tag}. 640 * @param member The target {@link Member}. 641 * @param node The child {@link Node} (may be 642 * {@code null}). 643 * 644 * @return {@link org.w3c.dom.Element} 645 */ 646 @Override 647 public FluentNode a(Tag tag, Member member, Node node) { 648 if (node == null) { 649 node = code(member.getName()); 650 } 651 652 return a(href(tag, member), node); 653 } 654 655 /** 656 * {@code <a href="}{@link ClassDoc type}{@code ">}{@link Node node}{@code </a>} 657 * 658 * @param tag The {@link Tag}. 659 * @param name The target {@link Class} name. 660 * @param node The child {@link Node} (may be 661 * {@code null}). 662 * 663 * @return {@link org.w3c.dom.Element} 664 */ 665 @Override 666 public FluentNode a(Tag tag, String name, Node node) { 667 ClassDoc target = getClassDocFor(tag, name); 668 669 if (node == null) { 670 node = code((target != null) ? target.name() : name); 671 } 672 673 return a(tag, target, node); 674 } 675 676 private URI href(Tag tag, ProgramElementDoc target) { 677 URI href = null; 678 679 if (target != null) { 680 ClassDoc classDoc = containingClass(target); 681 PackageDoc packageDoc = containingPackage(target); 682 683 if (target.isIncluded()) { 684 String path = "./"; 685 int depth = 686 countMatches(containingPackage(tag).name(), ".") + 1; 687 688 path += String.join(EMPTY, Collections.nCopies(depth, "../")); 689 690 if (isNotEmpty(packageDoc.name())) { 691 path += 692 String.join("/", 693 packageDoc.name().split(Pattern.quote("."))) 694 + "/"; 695 } 696 697 path += classDoc.name() + ".html"; 698 699 href = URI.create(path).normalize(); 700 } else { 701 if (configuration != null) { 702 DocLink link = 703 configuration.extern 704 .getExternalLink(packageDoc.name(), 705 null, classDoc.name() + ".html"); 706 /* 707 * Link might be null because the class cannot be 708 * loaded. 709 */ 710 if (link != null) { 711 href = URI.create(link.toString()); 712 } 713 } 714 } 715 } 716 717 if (href != null) { 718 if (target instanceof MemberDoc) { 719 String fragment = "#" + target.name(); 720 721 if (target instanceof ExecutableMemberDoc) { 722 fragment += 723 ((ExecutableMemberDoc) target).signature() 724 .replaceAll("[(),]", "-"); 725 } 726 727 href = href.resolve(fragment); 728 } 729 } 730 731 return href; 732 } 733 734 private URI href(Tag tag, Class<?> type) { 735 URI href = null; 736 ClassDoc target = getClassDocFor(tag, type); 737 738 if (target != null) { 739 href = href(tag, target); 740 } 741 742 return href; 743 } 744 745 private URI href(Tag tag, Member member) { 746 URI href = null; 747 ProgramElementDoc target = null; 748 749 if (member instanceof Field) { 750 target = getFieldDocFor(tag, (Field) member); 751 } else if (member instanceof Constructor) { 752 target = getConstructorDocFor(tag, (Constructor) member); 753 } else if (member instanceof Method) { 754 target = getMethodDocFor(tag, (Method) member); 755 } 756 757 if (target != null) { 758 href = href(tag, target); 759 } 760 761 return href; 762 } 763}