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}