001/*
002 * Copyright (c) 2003, 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 java.util.ArrayList;
029import java.util.EnumSet;
030import java.util.List;
031import java.util.Set;
032
033import javax.lang.model.element.AnnotationMirror;
034import javax.lang.model.element.Element;
035import javax.lang.model.element.TypeElement;
036import javax.lang.model.element.TypeParameterElement;
037import javax.lang.model.type.ArrayType;
038import javax.lang.model.type.DeclaredType;
039import javax.lang.model.type.TypeMirror;
040import javax.lang.model.type.TypeVariable;
041import javax.lang.model.type.WildcardType;
042import javax.lang.model.util.SimpleTypeVisitor14;
043
044import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.ContentBuilder;
045import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlTree;
046import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.TagName;
047import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Text;
048import org.jdrupes.mdoclet.internal.doclets.toolkit.BaseConfiguration;
049import org.jdrupes.mdoclet.internal.doclets.toolkit.Content;
050import org.jdrupes.mdoclet.internal.doclets.toolkit.Resources;
051import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPath;
052import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPaths;
053import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Utils;
054import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Utils.ElementFlag;
055
056/**
057 * A factory that returns a link given the information about it.
058 */
059public class HtmlLinkFactory {
060
061    private final HtmlDocletWriter m_writer;
062    private final DocPaths docPaths;
063    private final Utils utils;
064
065    /**
066     * Constructs a new link factory.
067     *
068     * @param writer the HTML doclet writer
069     */
070    public HtmlLinkFactory(HtmlDocletWriter writer) {
071        m_writer = writer;
072        docPaths = writer.configuration.docPaths;
073        utils = writer.configuration.utils;
074    }
075
076    /**
077     * {@return a new instance of a content object}
078     */
079    protected Content newContent() {
080        return new ContentBuilder();
081    }
082
083    /**
084     * Constructs a link from the given link information.
085     *
086     * @param linkInfo the information about the link.
087     * @return the link.
088     */
089    public Content getLink(HtmlLinkInfo linkInfo) {
090        if (linkInfo.getType() != null) {
091            SimpleTypeVisitor14<Content, HtmlLinkInfo> linkVisitor
092                = new SimpleTypeVisitor14<>() {
093
094                    final Content link = newContent();
095
096                    // handles primitives, no types and error types
097                    @Override
098                    protected Content defaultAction(TypeMirror type,
099                            HtmlLinkInfo linkInfo) {
100                        link.add(utils.getTypeName(type, false));
101                        return link;
102                    }
103
104                    int currentDepth = 0;
105
106                    @Override
107                    public Content visitArray(ArrayType type,
108                            HtmlLinkInfo linkInfo) {
109                        // keep track of the dimension depth and replace the
110                        // last dimension
111                        // specifier with varargs, when the stack is fully
112                        // unwound.
113                        currentDepth++;
114                        var componentType = type.getComponentType();
115                        visit(componentType, linkInfo.forType(componentType));
116                        currentDepth--;
117                        if (utils.isAnnotated(type)) {
118                            link.add(" ");
119                            link.add(getTypeAnnotationLinks(linkInfo));
120                        }
121                        // use vararg if required
122                        if (linkInfo.isVarArg() && currentDepth == 0) {
123                            link.add("...");
124                        } else {
125                            link.add("[]");
126                        }
127                        return link;
128                    }
129
130                    @Override
131                    public Content visitWildcard(WildcardType type,
132                            HtmlLinkInfo linkInfo) {
133                        link.add(getTypeAnnotationLinks(linkInfo));
134                        link.add("?");
135                        TypeMirror extendsBound = type.getExtendsBound();
136                        if (extendsBound != null) {
137                            link.add(" extends ");
138                            link.add(getLink(
139                                getBoundsLinkInfo(linkInfo, extendsBound)));
140                        }
141                        TypeMirror superBound = type.getSuperBound();
142                        if (superBound != null) {
143                            link.add(" super ");
144                            link.add(getLink(
145                                getBoundsLinkInfo(linkInfo, superBound)));
146                        }
147                        return link;
148                    }
149
150                    @Override
151                    public Content visitTypeVariable(TypeVariable type,
152                            HtmlLinkInfo linkInfo) {
153                        link.add(getTypeAnnotationLinks(linkInfo));
154                        TypeVariable typevariable = (utils.isArrayType(type))
155                            ? (TypeVariable) utils.getComponentType(type)
156                            : type;
157                        Element owner
158                            = typevariable.asElement().getEnclosingElement();
159                        if (linkInfo.linkTypeParameters()
160                            && utils.isTypeElement(owner)) {
161                            linkInfo.setTypeElement((TypeElement) owner);
162                            Content label = newContent();
163                            label.add(utils.getTypeName(type, false));
164                            linkInfo.label(label).skipPreview(true);
165                            link.add(getClassLink(linkInfo));
166                        } else {
167                            // No need to link method type parameters.
168                            link.add(utils.getTypeName(typevariable, false));
169                        }
170
171                        if (linkInfo.showTypeBounds()) {
172                            linkInfo.showTypeBounds(false);
173                            TypeParameterElement tpe
174                                = ((TypeParameterElement) typevariable
175                                    .asElement());
176                            boolean more = false;
177                            List<? extends TypeMirror> bounds
178                                = utils.getBounds(tpe);
179                            for (TypeMirror bound : bounds) {
180                                // we get everything as extends java.lang.Object
181                                // we suppress
182                                // all of them except those that have multiple
183                                // extends
184                                if (bounds.size() == 1 &&
185                                    utils.typeUtils.isSameType(bound,
186                                        utils.getObjectType())
187                                    &&
188                                    !utils.isAnnotated(bound)) {
189                                    continue;
190                                }
191                                link.add(more ? " & " : " extends ");
192                                link.add(getLink(
193                                    getBoundsLinkInfo(linkInfo, bound)));
194                                more = true;
195                            }
196                        }
197                        return link;
198                    }
199
200                    @Override
201                    public Content visitDeclared(DeclaredType type,
202                            HtmlLinkInfo linkInfo) {
203                        TypeMirror enc = type.getEnclosingType();
204                        if (enc instanceof DeclaredType dt
205                            && utils.isGenericType(dt)) {
206                            // If an enclosing type has type parameters render
207                            // them as separate links as
208                            // otherwise this information is lost. On the other
209                            // hand, plain enclosing types
210                            // are not linked separately as they are easy to
211                            // reach from the nested type.
212                            visitDeclared(dt, linkInfo.forType(dt));
213                            link.add(".");
214                        }
215                        link.add(getTypeAnnotationLinks(linkInfo));
216                        linkInfo.setTypeElement(utils.asTypeElement(type));
217                        link.add(getClassLink(linkInfo));
218                        if (linkInfo.showTypeParameters()) {
219                            link.add(getTypeParameterLinks(linkInfo));
220                        }
221                        return link;
222                    }
223                };
224            return linkVisitor.visit(linkInfo.getType(), linkInfo);
225        } else if (linkInfo.getTypeElement() != null) {
226            Content link = newContent();
227            link.add(getClassLink(linkInfo));
228            if (linkInfo.showTypeParameters()) {
229                link.add(getTypeParameterLinks(linkInfo));
230            }
231            return link;
232        } else {
233            return null;
234        }
235    }
236
237    /**
238     * Returns a link to the given class.
239     *
240     * @param linkInfo the information about the link to construct
241     * @return the link for the given class.
242     */
243    protected Content getClassLink(HtmlLinkInfo linkInfo) {
244        BaseConfiguration configuration = m_writer.configuration;
245        TypeElement typeElement = linkInfo.getTypeElement();
246        // Create a tool tip if we are linking to a class or interface. Don't
247        // create one if we are linking to a member.
248        String title = "";
249        String fragment = linkInfo.getFragment();
250        boolean hasFragment = fragment != null && !fragment.isEmpty();
251        if (!hasFragment) {
252            boolean isTypeLink = linkInfo.getType() != null &&
253                utils
254                    .isTypeVariable(utils.getComponentType(linkInfo.getType()));
255            title = getClassToolTip(typeElement, isTypeLink);
256        }
257        Content label = linkInfo.getClassLinkLabel(configuration);
258        if (linkInfo
259            .getContext() == HtmlLinkInfo.Kind.SHOW_TYPE_PARAMS_IN_LABEL) {
260            // For this kind of link, type parameters are included in the link
261            // label
262            // (and obviously not added after the link).
263            label.add(getTypeParameterLinks(linkInfo));
264        }
265        Set<ElementFlag> flags;
266        Element previewTarget;
267        boolean showPreview = !linkInfo.isSkipPreview();
268        if (!hasFragment && showPreview) {
269            flags = utils.elementFlags(typeElement);
270            previewTarget = typeElement;
271        } else if (linkInfo.getContext() == HtmlLinkInfo.Kind.SHOW_PREVIEW
272            && linkInfo.getTargetMember() != null && showPreview) {
273            flags = utils.elementFlags(linkInfo.getTargetMember());
274            TypeElement enclosing
275                = utils.getEnclosingTypeElement(linkInfo.getTargetMember());
276            Set<ElementFlag> enclosingFlags = utils.elementFlags(enclosing);
277            if (flags.contains(ElementFlag.PREVIEW)
278                && enclosingFlags.contains(ElementFlag.PREVIEW)) {
279                if (enclosing.equals(m_writer.getCurrentPageElement())) {
280                    // skip the PREVIEW tag:
281                    flags = EnumSet.copyOf(flags);
282                    flags.remove(ElementFlag.PREVIEW);
283                }
284                previewTarget = enclosing;
285            } else {
286                previewTarget = linkInfo.getTargetMember();
287            }
288        } else {
289            flags = EnumSet.noneOf(ElementFlag.class);
290            previewTarget = null;
291        }
292
293        Content link = new ContentBuilder();
294        if (utils.isIncluded(typeElement)) {
295            if (configuration.isGeneratedDoc(typeElement)
296                && !utils.hasHiddenTag(typeElement)) {
297                DocPath filename = getPath(linkInfo);
298                if (linkInfo.linkToSelf()
299                    || typeElement != m_writer.getCurrentPageElement()) {
300                    link.add(m_writer.links.createLink(
301                        filename.fragment(linkInfo.getFragment()),
302                        label,
303                        linkInfo.getStyle(),
304                        title));
305                    if (flags.contains(ElementFlag.PREVIEW)) {
306                        link.add(HtmlTree.SUP(m_writer.links.createLink(
307                            filename.fragment(m_writer.htmlIds
308                                .forPreviewSection(previewTarget).name()),
309                            m_writer.contents.previewMark)));
310                    }
311                    return link;
312                }
313            }
314        } else {
315            Content crossLink = m_writer.getCrossClassLink(
316                typeElement, linkInfo.getFragment(),
317                label, linkInfo.getStyle(), true);
318            if (crossLink != null) {
319                link.add(crossLink);
320                if (flags.contains(ElementFlag.PREVIEW)) {
321                    link.add(HtmlTree.SUP(m_writer.getCrossClassLink(
322                        typeElement,
323                        m_writer.htmlIds.forPreviewSection(previewTarget)
324                            .name(),
325                        m_writer.contents.previewMark,
326                        null, false)));
327                }
328                return link;
329            }
330        }
331        // Can't link so just write label.
332        link.add(label);
333        if (flags.contains(ElementFlag.PREVIEW)) {
334            link.add(HtmlTree.SUP(m_writer.contents.previewMark));
335        }
336        return link;
337    }
338
339    /**
340     * Returns links to the type parameters.
341     *
342     * @param linkInfo the information about the link to construct
343     * @return the links to the type parameters
344     */
345    protected Content getTypeParameterLinks(HtmlLinkInfo linkInfo) {
346        Content links = newContent();
347        List<TypeMirror> vars = new ArrayList<>();
348        TypeMirror ctype = linkInfo.getType() != null
349            ? utils.getComponentType(linkInfo.getType())
350            : null;
351        if (linkInfo.getExecutableElement() != null) {
352            linkInfo.getExecutableElement().getTypeParameters()
353                .forEach(t -> vars.add(t.asType()));
354        } else if (linkInfo.getType() != null
355            && utils.isDeclaredType(linkInfo.getType())) {
356            vars.addAll(((DeclaredType) linkInfo.getType()).getTypeArguments());
357        } else if (ctype != null && utils.isDeclaredType(ctype)) {
358            vars.addAll(((DeclaredType) ctype).getTypeArguments());
359        } else if (ctype == null && linkInfo.getTypeElement() != null) {
360            linkInfo.getTypeElement().getTypeParameters()
361                .forEach(t -> vars.add(t.asType()));
362        } else {
363            // Nothing to document.
364            return links;
365        }
366        if (!vars.isEmpty()) {
367            if (linkInfo.addLineBreakOpportunitiesInTypeParameters()) {
368                links.add(new HtmlTree(TagName.WBR));
369            }
370            links.add("<");
371            boolean many = false;
372            for (TypeMirror t : vars) {
373                if (many) {
374                    links.add(",");
375                    links.add(new HtmlTree(TagName.WBR));
376                    if (linkInfo.addLineBreaksInTypeParameters()) {
377                        links.add(Text.NL);
378                    }
379                }
380                links.add(getLink(linkInfo.forType(t)));
381                many = true;
382            }
383            links.add(">");
384        }
385        return links;
386    }
387
388    /**
389     * Returns links to the type annotations.
390     *
391     * @param linkInfo the information about the link to construct
392     * @return the links to the type annotations
393     */
394    public Content getTypeAnnotationLinks(HtmlLinkInfo linkInfo) {
395        ContentBuilder links = new ContentBuilder();
396        List<? extends AnnotationMirror> annotations;
397        if (utils.isAnnotated(linkInfo.getType())) {
398            annotations = linkInfo.getType().getAnnotationMirrors();
399        } else if (utils.isTypeVariable(linkInfo.getType())
400            && linkInfo.showTypeParameterAnnotations()) {
401            Element element = utils.typeUtils.asElement(linkInfo.getType());
402            annotations = element.getAnnotationMirrors();
403        } else {
404            return links;
405        }
406
407        if (annotations.isEmpty())
408            return links;
409
410        m_writer.getAnnotations(annotations, false)
411            .forEach(a -> {
412                links.add(a);
413                links.add(" ");
414            });
415
416        return links;
417    }
418
419    /*
420     * Returns a link info for a type bounds link.
421     */
422    private HtmlLinkInfo getBoundsLinkInfo(HtmlLinkInfo linkInfo,
423            TypeMirror bound) {
424        return linkInfo.forType(bound).skipPreview(false);
425    }
426
427    /**
428     * Given a class, return the appropriate tool tip.
429     *
430     * @param typeElement the class to get the tool tip for.
431     * @return the tool tip for the appropriate class.
432     */
433    private String getClassToolTip(TypeElement typeElement,
434            boolean isTypeLink) {
435        Resources resources = m_writer.configuration.getDocResources();
436        if (isTypeLink) {
437            return resources.getText("doclet.Href_Type_Param_Title",
438                utils.getSimpleName(typeElement));
439        } else if (utils.isPlainInterface(typeElement)) {
440            return resources.getText("doclet.Href_Interface_Title",
441                m_writer.getLocalizedPackageName(
442                    utils.containingPackage(typeElement)));
443        } else if (utils.isAnnotationInterface(typeElement)) {
444            return resources.getText("doclet.Href_Annotation_Title",
445                m_writer.getLocalizedPackageName(
446                    utils.containingPackage(typeElement)));
447        } else if (utils.isEnum(typeElement)) {
448            return resources.getText("doclet.Href_Enum_Title",
449                m_writer.getLocalizedPackageName(
450                    utils.containingPackage(typeElement)));
451        } else {
452            return resources.getText("doclet.Href_Class_Title",
453                m_writer.getLocalizedPackageName(
454                    utils.containingPackage(typeElement)));
455        }
456    }
457
458    /**
459     * Return path to the given file name in the given package. So if the name
460     * passed is "Object.html" and the name of the package is "java.lang", and
461     * if the relative path is "../.." then returned string will be
462     * "../../java/lang/Object.html"
463     *
464     * @param linkInfo the information about the link.
465     */
466    private DocPath getPath(HtmlLinkInfo linkInfo) {
467        return m_writer.pathToRoot
468            .resolve(docPaths.forClass(linkInfo.getTypeElement()));
469    }
470}