001/*
002 * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package org.jdrupes.mdoclet.internal.doclets.formats.html;
027
028import jdk.javadoc.doclet.DocletEnvironment;
029
030import javax.lang.model.element.Element;
031import javax.lang.model.element.ElementKind;
032import javax.lang.model.element.Modifier;
033import javax.lang.model.element.ModuleElement;
034import javax.lang.model.element.PackageElement;
035import javax.lang.model.element.RecordComponentElement;
036import javax.lang.model.element.TypeElement;
037import javax.lang.model.type.TypeMirror;
038import javax.lang.model.util.ElementKindVisitor14;
039
040import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.ContentBuilder;
041import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Entity;
042import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlStyle;
043import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlTree;
044import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.TagName;
045import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Text;
046import org.jdrupes.mdoclet.internal.doclets.toolkit.Content;
047import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Utils;
048
049import java.util.ArrayList;
050import java.util.List;
051import java.util.Set;
052import java.util.SortedSet;
053import java.util.TreeSet;
054import java.util.stream.Collectors;
055
056import static javax.lang.model.element.Modifier.ABSTRACT;
057import static javax.lang.model.element.Modifier.FINAL;
058import static javax.lang.model.element.Modifier.NATIVE;
059import static javax.lang.model.element.Modifier.PRIVATE;
060import static javax.lang.model.element.Modifier.PROTECTED;
061import static javax.lang.model.element.Modifier.PUBLIC;
062import static javax.lang.model.element.Modifier.STATIC;
063import static javax.lang.model.element.Modifier.STRICTFP;
064import static javax.lang.model.element.Modifier.SYNCHRONIZED;
065
066public class Signatures {
067
068    public static Content getModuleSignature(ModuleElement mdle,
069            ModuleWriterImpl moduleWriter) {
070        var signature = HtmlTree.DIV(HtmlStyle.moduleSignature);
071        Content annotations = moduleWriter.getAnnotationInfo(mdle, true);
072        if (!annotations.isEmpty()) {
073            signature.add(HtmlTree.SPAN(HtmlStyle.annotations, annotations));
074        }
075        DocletEnvironment docEnv = moduleWriter.configuration.docEnv;
076        String label = mdle.isOpen()
077            && (docEnv.getModuleMode() == DocletEnvironment.ModuleMode.ALL)
078                ? "open module"
079                : "module";
080        signature.add(label);
081        signature.add(" ");
082        var nameSpan = HtmlTree.SPAN(HtmlStyle.elementName);
083        nameSpan.add(mdle.getQualifiedName().toString());
084        signature.add(nameSpan);
085        return signature;
086    }
087
088    public static Content getPackageSignature(PackageElement pkg,
089            PackageWriterImpl pkgWriter) {
090        if (pkg.isUnnamed()) {
091            return Text.EMPTY;
092        }
093        var signature = HtmlTree.DIV(HtmlStyle.packageSignature);
094        Content annotations = pkgWriter.getAnnotationInfo(pkg, true);
095        if (!annotations.isEmpty()) {
096            signature.add(HtmlTree.SPAN(HtmlStyle.annotations, annotations));
097        }
098        signature.add("package ");
099        var nameSpan = HtmlTree.SPAN(HtmlStyle.elementName);
100        nameSpan.add(pkg.getQualifiedName().toString());
101        signature.add(nameSpan);
102        return signature;
103    }
104
105    static class TypeSignature {
106
107        private final TypeElement typeElement;
108        private final HtmlDocletWriter writer;
109        private final Utils utils;
110        private final HtmlConfiguration configuration;
111        private Content modifiers;
112
113        private static final Set<String> previewModifiers = Set.of();
114
115        TypeSignature(TypeElement typeElement, HtmlDocletWriter writer) {
116            this.typeElement = typeElement;
117            this.writer = writer;
118            this.utils = writer.utils;
119            this.configuration = writer.configuration;
120            this.modifiers = markPreviewModifiers(getModifiers());
121        }
122
123        public TypeSignature setModifiers(Content modifiers) {
124            this.modifiers = modifiers;
125            return this;
126        }
127
128        public Content toContent() {
129            Content content = new ContentBuilder();
130            Content annotationInfo
131                = writer.getAnnotationInfo(typeElement, true);
132            if (!annotationInfo.isEmpty()) {
133                content
134                    .add(HtmlTree.SPAN(HtmlStyle.annotations, annotationInfo));
135            }
136            content.add(HtmlTree.SPAN(HtmlStyle.modifiers, modifiers));
137
138            var nameSpan = HtmlTree.SPAN(HtmlStyle.elementName);
139            Content className = Text.of(utils.getSimpleName(typeElement));
140            if (configuration.getOptions().linkSource()) {
141                writer.addSrcLink(typeElement, className, nameSpan);
142            } else {
143                nameSpan.addStyle(HtmlStyle.typeNameLabel).add(className);
144            }
145            HtmlLinkInfo linkInfo = new HtmlLinkInfo(configuration,
146                HtmlLinkInfo.Kind.SHOW_TYPE_PARAMS_AND_BOUNDS, typeElement)
147                    .linkToSelf(false)  // Let's not link to ourselves in the
148                                        // signature
149                    .showTypeParameterAnnotations(true);
150            nameSpan.add(writer.getTypeParameterLinks(linkInfo));
151            content.add(nameSpan);
152
153            if (utils.isRecord(typeElement)) {
154                content.add(getRecordComponents());
155            }
156            if (!utils.isAnnotationInterface(typeElement)) {
157                var extendsImplements
158                    = HtmlTree.SPAN(HtmlStyle.extendsImplements);
159                if (!utils.isPlainInterface(typeElement)) {
160                    TypeMirror superclass
161                        = utils.getFirstVisibleSuperClass(typeElement);
162                    if (superclass != null) {
163                        content.add(Text.NL);
164                        extendsImplements.add("extends ");
165                        Content link
166                            = writer.getLink(new HtmlLinkInfo(configuration,
167                                HtmlLinkInfo.Kind.SHOW_TYPE_PARAMS,
168                                superclass));
169                        extendsImplements.add(link);
170                    }
171                }
172                List<? extends TypeMirror> interfaces
173                    = typeElement.getInterfaces();
174                if (!interfaces.isEmpty()) {
175                    boolean isFirst = true;
176                    for (TypeMirror type : interfaces) {
177                        TypeElement tDoc = utils.asTypeElement(type);
178                        if (!(utils.isPublic(tDoc) || utils.isLinkable(tDoc))) {
179                            continue;
180                        }
181                        if (isFirst) {
182                            extendsImplements.add(Text.NL);
183                            extendsImplements.add(
184                                utils.isPlainInterface(typeElement) ? "extends "
185                                    : "implements ");
186                            isFirst = false;
187                        } else {
188                            extendsImplements.add(", ");
189                        }
190                        Content link
191                            = writer.getLink(new HtmlLinkInfo(configuration,
192                                HtmlLinkInfo.Kind.SHOW_TYPE_PARAMS,
193                                type));
194                        extendsImplements.add(link);
195                    }
196                }
197                if (!extendsImplements.isEmpty()) {
198                    content.add(extendsImplements);
199                }
200            }
201            List<? extends TypeMirror> permits
202                = typeElement.getPermittedSubclasses();
203            List<? extends TypeMirror> linkablePermits = permits.stream()
204                .filter(t -> utils.isLinkable(utils.asTypeElement(t)))
205                .toList();
206            if (!linkablePermits.isEmpty()) {
207                var permitsSpan = HtmlTree.SPAN(HtmlStyle.permits);
208                boolean isFirst = true;
209                for (TypeMirror type : linkablePermits) {
210                    if (isFirst) {
211                        content.add(Text.NL);
212                        permitsSpan.add("permits");
213                        permitsSpan.add(" ");
214                        isFirst = false;
215                    } else {
216                        permitsSpan.add(", ");
217                    }
218                    Content link
219                        = writer.getLink(new HtmlLinkInfo(configuration,
220                            HtmlLinkInfo.Kind.SHOW_TYPE_PARAMS,
221                            type));
222                    permitsSpan.add(link);
223                }
224                if (linkablePermits.size() < permits.size()) {
225                    Content c = Text.of(configuration.getDocResources()
226                        .getText("doclet.not.exhaustive"));
227                    permitsSpan.add(" ");
228                    permitsSpan.add(HtmlTree.SPAN(HtmlStyle.permitsNote, c));
229                }
230                content.add(permitsSpan);
231            }
232            return HtmlTree.DIV(HtmlStyle.typeSignature, content);
233        }
234
235        private Content getRecordComponents() {
236            Content content = new ContentBuilder();
237            content.add("(");
238            String sep = "";
239            for (RecordComponentElement e : typeElement.getRecordComponents()) {
240                content.add(sep);
241                writer.getAnnotations(e.getAnnotationMirrors(), false)
242                    .forEach(a -> content.add(a).add(" "));
243                Content link = writer.getLink(new HtmlLinkInfo(configuration,
244                    HtmlLinkInfo.Kind.LINK_TYPE_PARAMS_AND_BOUNDS,
245                    e.asType()));
246                content.add(link);
247                content.add(Entity.NO_BREAK_SPACE);
248                content.add(e.getSimpleName());
249                sep = ", ";
250            }
251            content.add(")");
252            return content;
253        }
254
255        private Content markPreviewModifiers(List<String> modifiers) {
256            Content content = new ContentBuilder();
257            String sep = null;
258            for (String modifier : modifiers) {
259                if (sep != null) {
260                    content.add(sep);
261                }
262                content.add(modifier);
263                if (previewModifiers.contains(modifier)) {
264                    content.add(HtmlTree.SUP(writer.links.createLink(
265                        configuration.htmlIds.forPreviewSection(typeElement),
266                        configuration.contents.previewMark)));
267                }
268                sep = " ";
269            }
270            content.add(" ");
271            return content;
272        }
273
274        private List<String> getModifiers() {
275            SortedSet<Modifier> modifiers
276                = new TreeSet<>(typeElement.getModifiers());
277            modifiers.remove(NATIVE);
278            modifiers.remove(STRICTFP);
279            modifiers.remove(SYNCHRONIZED);
280
281            return new ElementKindVisitor14<List<String>,
282                    SortedSet<Modifier>>() {
283                final List<String> list = new ArrayList<>();
284
285                void addVisibilityModifier(Set<Modifier> modifiers) {
286                    if (modifiers.contains(PUBLIC)) {
287                        list.add("public");
288                    } else if (modifiers.contains(PROTECTED)) {
289                        list.add("protected");
290                    } else if (modifiers.contains(PRIVATE)) {
291                        list.add("private");
292                    }
293                }
294
295                void addStatic(Set<Modifier> modifiers) {
296                    if (modifiers.contains(STATIC)) {
297                        list.add("static");
298                    }
299                }
300
301                void addSealed(TypeElement e) {
302                    if (e.getModifiers().contains(Modifier.SEALED)) {
303                        list.add("sealed");
304                    } else if (e.getModifiers().contains(Modifier.NON_SEALED)) {
305                        list.add("non-sealed");
306                    }
307                }
308
309                void addModifiers(Set<Modifier> modifiers) {
310                    modifiers.stream()
311                        .map(Modifier::toString)
312                        .forEachOrdered(list::add);
313                }
314
315                @Override
316                public List<String> visitTypeAsInterface(TypeElement e,
317                        SortedSet<Modifier> mods) {
318                    addVisibilityModifier(mods);
319                    addStatic(mods);
320                    addSealed(e);
321                    list.add("interface");
322                    return list;
323                }
324
325                @Override
326                public List<String> visitTypeAsEnum(TypeElement e,
327                        SortedSet<Modifier> mods) {
328                    addVisibilityModifier(mods);
329                    addStatic(mods);
330                    list.add("enum");
331                    return list;
332                }
333
334                @Override
335                public List<String> visitTypeAsAnnotationType(TypeElement e,
336                        SortedSet<Modifier> mods) {
337                    addVisibilityModifier(mods);
338                    addStatic(mods);
339                    list.add("@interface");
340                    return list;
341                }
342
343                @Override
344                public List<String> visitTypeAsRecord(TypeElement e,
345                        SortedSet<Modifier> mods) {
346                    mods.remove(FINAL); // suppress the implicit `final`
347                    return visitTypeAsClass(e, mods);
348                }
349
350                @Override
351                public List<String> visitTypeAsClass(TypeElement e,
352                        SortedSet<Modifier> mods) {
353                    addModifiers(mods);
354                    String keyword
355                        = e.getKind() == ElementKind.RECORD ? "record"
356                            : "class";
357                    list.add(keyword);
358                    return list;
359                }
360
361                @Override
362                protected List<String> defaultAction(Element e,
363                        SortedSet<Modifier> mods) {
364                    addModifiers(mods);
365                    return list;
366                }
367
368            }.visit(typeElement, modifiers);
369        }
370    }
371
372    /**
373     * A content builder for member signatures.
374     */
375    static class MemberSignature {
376
377        private final AbstractMemberWriter memberWriter;
378        private final Utils utils;
379
380        private final Element element;
381        private Content annotations;
382        private Content typeParameters;
383        private Content returnType;
384        private Content parameters;
385        private Content exceptions;
386
387        // Threshold for length of type parameters before switching from inline
388        // to block representation.
389        private static final int TYPE_PARAMS_MAX_INLINE_LENGTH = 50;
390
391        // Threshold for combined length of modifiers, type params and return
392        // type before breaking
393        // it up with a line break before the return type.
394        private static final int RETURN_TYPE_MAX_LINE_LENGTH = 50;
395
396        /**
397         * Creates a new member signature builder.
398         *
399         * @param element the element for which to create a signature
400         * @param memberWriter the member writer
401         */
402        MemberSignature(Element element, AbstractMemberWriter memberWriter) {
403            this.element = element;
404            this.memberWriter = memberWriter;
405            this.utils = memberWriter.utils;
406        }
407
408        /**
409         * Set the type parameters for an executable member.
410         *
411         * @param typeParameters the type parameters to add.
412         * @return this instance
413         */
414        MemberSignature setTypeParameters(Content typeParameters) {
415            this.typeParameters = typeParameters;
416            return this;
417        }
418
419        /**
420         * Set the return type for an executable member.
421         *
422         * @param returnType the return type to add.
423         * @return this instance
424         */
425        MemberSignature setReturnType(Content returnType) {
426            this.returnType = returnType;
427            return this;
428        }
429
430        /**
431         * Set the type information for a non-executable member.
432         *
433         * @param type the type of the member.
434         * @return this instance
435         */
436        MemberSignature setType(TypeMirror type) {
437            this.returnType = memberWriter.writer
438                .getLink(new HtmlLinkInfo(memberWriter.configuration,
439                    HtmlLinkInfo.Kind.LINK_TYPE_PARAMS_AND_BOUNDS, type));
440            return this;
441        }
442
443        /**
444         * Set the parameter information of an executable member.
445         *
446         * @param content the parameter information.
447         * @return this instance
448         */
449        MemberSignature setParameters(Content content) {
450            this.parameters = content;
451            return this;
452        }
453
454        /**
455         * Set the exception information of an executable member.
456         *
457         * @param content the exception information
458         * @return this instance
459         */
460        MemberSignature setExceptions(Content content) {
461            this.exceptions = content;
462            return this;
463        }
464
465        /**
466         * Set the annotation information of a member.
467         *
468         * @param content the exception information
469         * @return this instance
470         */
471        MemberSignature setAnnotations(Content content) {
472            this.annotations = content;
473            return this;
474        }
475
476        /**
477         * Returns an HTML tree containing the member signature.
478         *
479         * @return an HTML tree containing the member signature
480         */
481        Content toContent() {
482            Content content = new ContentBuilder();
483            // Position of last line separator.
484            int lastLineSeparator = 0;
485
486            // Annotations
487            if (annotations != null && !annotations.isEmpty()) {
488                content.add(HtmlTree.SPAN(HtmlStyle.annotations, annotations));
489                lastLineSeparator = content.charCount();
490            }
491
492            // Modifiers
493            appendModifiers(content);
494
495            // Type parameters
496            if (typeParameters != null && !typeParameters.isEmpty()) {
497                lastLineSeparator
498                    = appendTypeParameters(content, lastLineSeparator);
499            }
500
501            // Return type
502            if (returnType != null) {
503                content.add(HtmlTree.SPAN(HtmlStyle.returnType, returnType));
504                content.add(Entity.NO_BREAK_SPACE);
505            }
506
507            // Name
508            var nameSpan = HtmlTree.SPAN(HtmlStyle.elementName);
509            if (memberWriter.options.linkSource()) {
510                Content name = Text.of(memberWriter.name(element));
511                memberWriter.writer.addSrcLink(element, name, nameSpan);
512            } else {
513                nameSpan.add(memberWriter.name(element));
514            }
515            content.add(nameSpan);
516
517            // Parameters and exceptions
518            if (parameters != null) {
519                appendParametersAndExceptions(content, lastLineSeparator);
520            }
521
522            return HtmlTree.DIV(HtmlStyle.memberSignature, content);
523        }
524
525        /**
526         * Adds the modifiers for the member. The modifiers are ordered as specified
527         * by <em>The Java Language Specification</em>.
528         *
529         * @param target the content to which the modifier information will be added
530         */
531        private void appendModifiers(Content target) {
532            Set<Modifier> set = new TreeSet<>(element.getModifiers());
533
534            // remove the ones we really don't need
535            set.remove(NATIVE);
536            set.remove(SYNCHRONIZED);
537            set.remove(STRICTFP);
538
539            // According to JLS, we should not be showing public modifier for
540            // interface methods and fields.
541            if ((utils.isField(element) || utils.isMethod(element))) {
542                Element te = element.getEnclosingElement();
543                if (utils.isInterface(te)) {
544                    // Remove the implicit abstract and public modifiers
545                    if (utils.isMethod(element)) {
546                        set.remove(ABSTRACT);
547                    }
548                    set.remove(PUBLIC);
549                }
550            }
551            if (!set.isEmpty()) {
552                String mods = set.stream().map(Modifier::toString)
553                    .collect(Collectors.joining(" "));
554                target.add(HtmlTree.SPAN(HtmlStyle.modifiers, Text.of(mods)))
555                    .add(Entity.NO_BREAK_SPACE);
556            }
557        }
558
559        /**
560         * Appends the type parameter information to the HTML tree.
561         *
562         * @param target            the HTML tree
563         * @param lastLineSeparator index of last line separator in the HTML tree
564         * @return the new index of the last line separator
565         */
566        private int appendTypeParameters(Content target,
567                int lastLineSeparator) {
568            // Apply different wrapping strategies for type parameters
569            // depending on the combined length of type parameters and return
570            // type.
571            int typeParamLength = typeParameters.charCount();
572
573            if (typeParamLength >= TYPE_PARAMS_MAX_INLINE_LENGTH) {
574                target.add(HtmlTree.SPAN(HtmlStyle.typeParametersLong,
575                    typeParameters));
576            } else {
577                target.add(
578                    HtmlTree.SPAN(HtmlStyle.typeParameters, typeParameters));
579            }
580
581            int lineLength = target.charCount() - lastLineSeparator;
582            int newLastLineSeparator = lastLineSeparator;
583
584            // sum below includes length of modifiers plus type params added
585            // above
586            if (lineLength
587                + returnType.charCount() > RETURN_TYPE_MAX_LINE_LENGTH) {
588                target.add(Text.NL);
589                newLastLineSeparator = target.charCount();
590            } else {
591                target.add(Entity.NO_BREAK_SPACE);
592            }
593
594            return newLastLineSeparator;
595        }
596
597        /**
598         * Appends the parameters and exceptions information to the HTML tree.
599         *
600         * @param target            the HTML tree
601         * @param lastLineSeparator the index of the last line separator in the HTML tree
602         */
603        private void appendParametersAndExceptions(Content target,
604                int lastLineSeparator) {
605            // Record current position for indentation of exceptions
606            int indentSize = target.charCount() - lastLineSeparator;
607
608            if (parameters.charCount() == 2) {
609                // empty parameters are added without packing
610                target.add(parameters);
611            } else {
612                target.add(new HtmlTree(TagName.WBR))
613                    .add(HtmlTree.SPAN(HtmlStyle.parameters, parameters));
614            }
615
616            // Exceptions
617            if (exceptions != null && !exceptions.isEmpty()) {
618                CharSequence indent
619                    = " ".repeat(Math.max(0, indentSize + 1 - 7));
620                target.add(Text.NL)
621                    .add(indent)
622                    .add("throws ")
623                    .add(HtmlTree.SPAN(HtmlStyle.exceptions, exceptions));
624            }
625        }
626    }
627}