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.toolkit.util;
027
028import java.util.Objects;
029import javax.lang.model.element.Element;
030import javax.lang.model.element.ExecutableElement;
031import javax.lang.model.element.ModuleElement;
032import javax.lang.model.element.PackageElement;
033import javax.lang.model.element.TypeElement;
034import javax.lang.model.element.VariableElement;
035import javax.lang.model.util.SimpleElementVisitor14;
036
037import com.sun.source.doctree.DocTree;
038
039/**
040 * An item to be included in the index pages and in interactive search.
041 *
042 * <p>
043 * Items are primarily defined by their position in the documentation,
044 * which is one of:
045 *
046 * <ul>
047 * <li>An element (module, package, type or member)
048 * <li>One of a small set of tags in the doc comment for an element:
049 *     {@code {@index ...}}, {@code {@systemProperty ...}}, etc
050 * <li>One of a small set of outliers, corresponding to summary pages:
051 *     "All Classes", "All Packages", etc
052 * </ul>
053 *
054 * <p>
055 * All items have a "label", which is the presentation string used
056 * to display the item in the list of matching choices. The
057 * label is specified when the item is created.  Items also
058 * have a "url" and a "description", which are provided by
059 * the specific doclet.
060 *
061 * <p>
062 * Each item provides details to be included in the search index files
063 * read and processed by JavaScript.
064 * Items have a "category", which is normally derived from the element
065 * kind or doc tree kind; it corresponds to the JavaScript file
066 * in which this item will be written.
067 *
068 * <p>
069 * Items for an element may have one or more of the following:
070 * "containing module", "containing package", "containing type".
071 *
072 * <p>
073 * Items for a node in a doc tree have a "holder", which is a
074 * text form of the enclosing element or page.
075 * They will typically also have a "description" derived from
076 * content in the doc tree node.
077 */
078public class IndexItem {
079
080    /**
081     * The "category" used to group items for the interactive search index.
082     * Categories correspond directly to the JavaScript files that will be generated.
083     */
084    public enum Category {
085        MODULES,
086        PACKAGES,
087        TYPES,
088        MEMBERS,
089        TAGS
090    }
091
092    /**
093     * The presentation string for the item. It must be non-empty.
094     */
095    private final String label;
096
097    /**
098     * The element for the item. It is only null for items for summary pages that are not
099     * associated with any specific element.
100     *
101     */
102    private final Element element;
103
104    /**
105     * The URL pointing to the element, doc tree or page being indexed.
106     * It may be empty if the information can be determined from other fields.
107     */
108    private String url = "";
109
110    /**
111     * The containing module, if any, for the item.
112     * It will be empty if the element is not in a package, and may be omitted if the
113     * name of the package is unique.
114     */
115    private String containingModule = "";
116
117    /**
118     * The containing package, if any, for the item.
119     */
120    private String containingPackage = "";
121
122    /**
123     * The containing class, if any, for the item.
124     */
125    private String containingClass = "";
126
127    /**
128     * Creates an index item for a module element.
129     *
130     * @param moduleElement the element
131     * @param utils         the common utilities class
132     *
133     * @return the item
134     */
135    public static IndexItem of(ModuleElement moduleElement, Utils utils) {
136        return new IndexItem(moduleElement,
137            utils.getFullyQualifiedName(moduleElement));
138    }
139
140    /**
141     * Creates an index item for a package element.
142     *
143     * @param packageElement the element
144     * @param utils          the common utilities class
145     *
146     * @return the item
147     */
148    public static IndexItem of(PackageElement packageElement, Utils utils) {
149        return new IndexItem(packageElement,
150            utils.getPackageName(packageElement));
151    }
152
153    /**
154     * Creates an index item for a type element.
155     * Note: use {@code getElement()} to access this value, not {@code getTypeElement}.
156     *
157     * @param typeElement the element
158     * @param utils       the common utilities class
159     *
160     * @return the item
161     */
162    public static IndexItem of(TypeElement typeElement, Utils utils) {
163        return new IndexItem(typeElement, utils.getSimpleName(typeElement));
164    }
165
166    /**
167     * Creates an index item for a member element.
168     * Note: the given type element may not be the same as the enclosing element of the member
169     *       in cases where the enclosing element is not visible in the documentation.
170     *
171     * @param typeElement the element that contains the member
172     * @param member      the member
173     * @param utils       the common utilities class
174     *
175     * @return the item
176     *
177     * @see #getContainingTypeElement()
178     */
179    public static IndexItem of(TypeElement typeElement, Element member,
180            Utils utils) {
181        String name = utils.getSimpleName(member);
182        if (utils.isExecutableElement(member)) {
183            ExecutableElement ee = (ExecutableElement) member;
184            name += utils.flatSignature(ee, typeElement);
185        }
186        return new IndexItem(member, name) {
187            @Override
188            public TypeElement getContainingTypeElement() {
189                return typeElement;
190            }
191        };
192    }
193
194    /**
195     * Creates an index item for a node in the doc comment for an element.
196     * The node should only be one that gives rise to an entry in the index.
197     *
198     * @param element     the element
199     * @param docTree     the node in the doc comment
200     * @param label       the label
201     * @param holder      the holder for the comment
202     * @param description the description of the item
203     * @param link        the root-relative link to the item in the generated docs
204     *
205     * @return the item
206     */
207    public static IndexItem of(Element element, DocTree docTree, String label,
208            String holder, String description, DocLink link) {
209        Objects.requireNonNull(element);
210        Objects.requireNonNull(holder);
211        Objects.requireNonNull(description);
212        Objects.requireNonNull(link);
213
214        switch (docTree.getKind()) {
215        case INDEX, SPEC, SYSTEM_PROPERTY, START_ELEMENT -> {
216        }
217        default -> throw new IllegalArgumentException(
218            docTree.getKind().toString());
219        }
220
221        return new IndexItem(element, label, link.toString()) {
222            @Override
223            public DocTree getDocTree() {
224                return docTree;
225            }
226
227            @Override
228            public Category getCategory() {
229                return getCategory(docTree);
230            }
231
232            @Override
233            public String getHolder() {
234                return holder;
235            }
236
237            @Override
238            public String getDescription() {
239                return description;
240            }
241        };
242    }
243
244    /**
245     * Creates an index item for a summary page, that is not associated with any element or
246     * node in a doc comment.
247     *
248     * @param category the category for the item
249     * @param label the label for the item
250     * @param path the path for the page
251     *
252     * @return the item
253     */
254    public static IndexItem of(Category category, String label, DocPath path) {
255        Objects.requireNonNull(category);
256        return new IndexItem(null, label, path.getPath()) {
257            @Override
258            public DocTree getDocTree() {
259                return null;
260            }
261
262            @Override
263            public Category getCategory() {
264                return category;
265            }
266
267            @Override
268            public String getHolder() {
269                return "";
270            }
271
272            @Override
273            public String getDescription() {
274                return "";
275            }
276        };
277    }
278
279    private IndexItem(Element element, String label) {
280        if (label.isEmpty()) {
281            throw new IllegalArgumentException();
282        }
283        if (label.contains("\n") || label.contains("\r")) {
284            throw new IllegalArgumentException();
285        }
286
287        this.element = element;
288        this.label = label;
289    }
290
291    private IndexItem(Element element, String label, String url) {
292        this(element, label);
293        setUrl(url);
294    }
295
296    /**
297     * Returns the label of the item.
298     *
299     * @return the label
300     */
301    public String getLabel() {
302        return label;
303    }
304
305    /**
306     * Returns the part of the label after the last dot, or the whole label if there are no dots.
307     *
308     * @return the simple name
309     */
310    public String getSimpleName() {
311        return label.substring(label.lastIndexOf('.') + 1);
312    }
313
314    /**
315     * Returns the label with a fully-qualified type name.
316     * (Used to determine if labels are unique or need to be qualified.)
317     *
318     * @param utils the common utilities class
319     *
320     * @return the fully qualified name
321     */
322    public String getFullyQualifiedLabel(Utils utils) {
323        TypeElement typeElement = getContainingTypeElement();
324        if (typeElement != null) {
325            return utils.getFullyQualifiedName(typeElement) + "." + label;
326        } else if (isElementItem()) {
327            return utils.getFullyQualifiedName(element);
328        } else {
329            return label;
330        }
331    }
332
333    /**
334     * Returns the element associate with this item, or {@code null}.
335     *
336     * @return the element
337     */
338    public Element getElement() {
339        return element;
340    }
341
342    /**
343     * Returns the category for this item, that indicates the JavaScript file
344     * in which this item should be written.
345     *
346     * @return the category
347     */
348    public Category getCategory() {
349        return getCategory(element);
350    }
351
352    protected Category getCategory(DocTree docTree) {
353        return switch (docTree.getKind()) {
354        case INDEX, SPEC, SYSTEM_PROPERTY, START_ELEMENT -> Category.TAGS;
355        default -> throw new IllegalArgumentException(
356            docTree.getKind().toString());
357        };
358    }
359
360    protected Category getCategory(Element element) {
361        return new SimpleElementVisitor14<Category, Void>() {
362            @Override
363            public Category visitModule(ModuleElement t, Void v) {
364                return Category.MODULES;
365            }
366
367            @Override
368            public Category visitPackage(PackageElement e, Void v) {
369                return Category.PACKAGES;
370            }
371
372            @Override
373            public Category visitType(TypeElement e, Void v) {
374                return Category.TYPES;
375            }
376
377            @Override
378            public Category visitVariable(VariableElement e, Void v) {
379                return Category.MEMBERS;
380            }
381
382            @Override
383            public Category visitExecutable(ExecutableElement e, Void v) {
384                return Category.MEMBERS;
385            }
386
387            @Override
388            public Category defaultAction(Element e, Void v) {
389                throw new IllegalArgumentException(e.toString());
390            }
391        }.visit(element);
392    }
393
394    /**
395     * Returns the type element that is documented as containing a member element,
396     * or {@code null} if this item does not represent a member element.
397     *
398     * @return the type element
399     */
400    public TypeElement getContainingTypeElement() {
401        return null;
402    }
403
404    /**
405     * Returns the documentation tree node for this item, of {@code null} if this item
406     * does not represent a documentation tree node.
407     *
408     * @return the documentation tree node
409     */
410    public DocTree getDocTree() {
411        return null;
412    }
413
414    /**
415     * Returns {@code true} if this index is for an element.
416     *
417     * @return {@code true} if this index is for an element
418     */
419    public boolean isElementItem() {
420        return element != null && getDocTree() == null;
421    }
422
423    /**
424     * Returns {@code true} if this index is for a tag in a doc comment.
425     *
426     * @return {@code true} if this index is for a tag in a doc comment
427     */
428    public boolean isTagItem() {
429        return getDocTree() != null;
430    }
431
432    /**
433     * Returns {@code true} if this index is for a specific kind of tag in a doc comment.
434     *
435     * @return {@code true} if this index is for a specific kind of tag in a doc comment
436     */
437    public boolean isKind(DocTree.Kind kind) {
438        DocTree dt = getDocTree();
439        return dt != null && dt.getKind() == kind;
440    }
441
442    /**
443     * Sets the URL for the item, when it cannot otherwise be inferred from other fields.
444     *
445     * @param u the url
446     *
447     * @return this item
448     */
449    public IndexItem setUrl(String u) {
450        url = Objects.requireNonNull(u);
451        return this;
452    }
453
454    /**
455     * Returns the URL for this item, or an empty string if no value has been set.
456     *
457     * @return the URL for this item, or an empty string if no value has been set
458     */
459    public String getUrl() {
460        return url;
461    }
462
463    /**
464     * Sets the name of the containing module for this item.
465     *
466     * @param m the module
467     *
468     * @return this item
469     */
470    public IndexItem setContainingModule(String m) {
471        containingModule = Objects.requireNonNull(m);
472        return this;
473    }
474
475    /**
476     * Sets the name of the containing package for this item.
477     *
478     * @param p the package
479     *
480     * @return this item
481     */
482    public IndexItem setContainingPackage(String p) {
483        containingPackage = Objects.requireNonNull(p);
484        return this;
485    }
486
487    /**
488     * Sets the name of the containing class for this item.
489     *
490     * @param c the class
491     *
492     * @return this item
493     */
494    public IndexItem setContainingClass(String c) {
495        containingClass = Objects.requireNonNull(c);
496        return this;
497    }
498
499    /**
500     * Returns a description of the element owning the documentation comment for this item,
501     * or {@code null} if this is not a item for a tag for an item in a documentation tag.
502     *
503     * @return the description of the element that owns this item
504     */
505    public String getHolder() {
506        return null;
507    }
508
509    /**
510     * Returns a description of the tag for this item or {@code null} if this is not a item
511     * for a tag for an item in a documentation tag.
512     *
513     * @return the description of the tag
514     */
515    public String getDescription() {
516        return null;
517    }
518
519    /**
520     * Returns a string representing this item in JSON notation.
521     *
522     * @return a string representing this item in JSON notation
523     */
524    public String toJSON() {
525        // TODO: Additional processing is required, see JDK-8238495
526        StringBuilder item = new StringBuilder();
527        Category category = getCategory();
528        switch (category) {
529        case MODULES:
530            item.append("{")
531                .append("\"l\":\"").append(label).append("\"")
532                .append("}");
533            break;
534
535        case PACKAGES:
536            item.append("{");
537            if (!containingModule.isEmpty()) {
538                item.append("\"m\":\"").append(containingModule).append("\",");
539            }
540            item.append("\"l\":\"").append(label).append("\"");
541            if (!url.isEmpty()) {
542                item.append(",\"u\":\"").append(url).append("\"");
543            }
544            item.append("}");
545            break;
546
547        case TYPES:
548            item.append("{");
549            if (!containingPackage.isEmpty()) {
550                item.append("\"p\":\"").append(containingPackage).append("\",");
551            }
552            if (!containingModule.isEmpty()) {
553                item.append("\"m\":\"").append(containingModule).append("\",");
554            }
555            item.append("\"l\":\"").append(label).append("\"");
556            if (!url.isEmpty()) {
557                item.append(",\"u\":\"").append(url).append("\"");
558            }
559            item.append("}");
560            break;
561
562        case MEMBERS:
563            item.append("{");
564            if (!containingModule.isEmpty()) {
565                item.append("\"m\":\"").append(containingModule).append("\",");
566            }
567            item.append("\"p\":\"").append(containingPackage).append("\",")
568                .append("\"c\":\"").append(containingClass).append("\",")
569                .append("\"l\":\"").append(label).append("\"");
570            if (!url.isEmpty()) {
571                item.append(",\"u\":\"").append(url).append("\"");
572            }
573            item.append("}");
574            break;
575
576        case TAGS:
577            String holder = getHolder();
578            String description = getDescription();
579            item.append("{")
580                .append("\"l\":\"").append(escapeQuotes(label)).append("\",")
581                .append("\"h\":\"").append(holder).append("\",");
582            if (!description.isEmpty()) {
583                item.append("\"d\":\"").append(escapeQuotes(description))
584                    .append("\",");
585            }
586            item.append("\"u\":\"").append(escapeQuotes(url)).append("\"")
587                .append("}");
588            break;
589
590        default:
591            throw new AssertionError("Unexpected category: " + category);
592        }
593        return item.toString();
594    }
595
596    private String escapeQuotes(String s) {
597        return s.replace("\\", "\\\\").replace("\"", "\\\"");
598    }
599}