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}