001/*
002 * Copyright (c) 1998, 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 static org.jdrupes.mdoclet.internal.doclets.toolkit.util.VisibleMemberTable.Kind.*;
029
030import java.util.*;
031import java.util.stream.Collectors;
032
033import javax.lang.model.element.Element;
034import javax.lang.model.element.ModuleElement;
035import javax.lang.model.element.PackageElement;
036import javax.lang.model.element.TypeElement;
037
038import org.jdrupes.mdoclet.internal.doclets.toolkit.BaseConfiguration;
039import org.jdrupes.mdoclet.internal.doclets.toolkit.Messages;
040
041import com.sun.source.doctree.DocTree;
042
043/**
044 * An alphabetical index of elements, search tags, and other items.
045 * Two tables are maintained:
046 * one is indexed by the first character of each items name;
047 * the other is index by the item's category, indicating the JavaScript
048 * file in which the item should be written.
049 */
050public class IndexBuilder {
051
052    /**
053     * Sets of items keyed by the first character of the names (labels)
054     * of the items in those sets.
055     */
056    private final Map<Character, SortedSet<IndexItem>> itemsByFirstChar;
057
058    /**
059     * Sets of items keyed by the {@link IndexItem.Category category}
060     * of the items in those sets.
061     */
062    private final Map<IndexItem.Category, SortedSet<IndexItem>> itemsByCategory;
063
064    /**
065     * Don't generate deprecated information if true.
066     */
067    private final boolean noDeprecated;
068
069    /**
070     * Build this index only for classes?
071     */
072    protected final boolean classesOnly;
073
074    private final BaseConfiguration configuration;
075    private final Utils utils;
076
077    /**
078     * The comparator used for the sets in {@code itemsByFirstChar}.
079     */
080    private final Comparator<IndexItem> mainComparator;
081
082    /**
083     * Creates a new {@code IndexBuilder}.
084     *
085     * @param configuration the current configuration of the doclet
086     * @param noDeprecated  true if -nodeprecated option is used,
087     *                      false otherwise
088     */
089    public IndexBuilder(BaseConfiguration configuration,
090            boolean noDeprecated) {
091        this(configuration, noDeprecated, false);
092    }
093
094    /**
095     * Creates a new {@code IndexBuilder}.
096     *
097     * @param configuration the current configuration of the doclet
098     * @param noDeprecated  true if -nodeprecated option is used,
099     *                      false otherwise
100     * @param classesOnly   include only classes in index
101     */
102    public IndexBuilder(BaseConfiguration configuration,
103            boolean noDeprecated,
104            boolean classesOnly) {
105        this.configuration = configuration;
106        this.utils = configuration.utils;
107
108        Messages messages = configuration.getMessages();
109        if (classesOnly) {
110            messages.notice("doclet.Building_Index_For_All_Classes");
111        } else {
112            messages.notice("doclet.Building_Index");
113        }
114
115        this.noDeprecated = noDeprecated;
116        this.classesOnly = classesOnly;
117
118        itemsByFirstChar = new TreeMap<>();
119        itemsByCategory = new EnumMap<>(IndexItem.Category.class);
120
121        mainComparator
122            = classesOnly ? makeClassComparator() : makeIndexComparator();
123    }
124
125    /**
126     * Adds all the selected modules, packages, types and their members to the index,
127     * or just the type elements if {@code classesOnly} is {@code true}.
128     */
129    public void addElements() {
130        Set<TypeElement> classes = configuration.getIncludedTypeElements();
131        indexTypeElements(classes);
132        if (classesOnly) {
133            return;
134        }
135        Set<PackageElement> packages
136            = configuration.getSpecifiedPackageElements();
137        if (packages.isEmpty()) {
138            packages = classes
139                .stream()
140                .map(utils::containingPackage)
141                .filter(_package -> _package != null && !_package.isUnnamed())
142                .collect(Collectors.toSet());
143        }
144        packages.forEach(this::indexPackage);
145        classes.stream()
146            .filter(this::shouldIndex)
147            .forEach(this::indexMembers);
148
149        if (configuration.showModules) {
150            indexModules();
151        }
152    }
153
154    /**
155     * Adds an individual item to the two collections of items.
156     *
157     * @param item the item to add
158     */
159    public void add(IndexItem item) {
160        Objects.requireNonNull(item);
161
162        if (item.isElementItem() || item.isTagItem()) {
163            // don't put summary-page items in the A-Z index:
164            // they are listed separately, at the top of the index page
165            itemsByFirstChar.computeIfAbsent(keyCharacter(item.getLabel()),
166                c -> new TreeSet<>(mainComparator))
167                .add(item);
168        }
169
170        itemsByCategory.computeIfAbsent(item.getCategory(),
171            c -> new TreeSet<>(c == IndexItem.Category.TYPES
172                ? makeTypeSearchIndexComparator()
173                : makeGenericSearchIndexComparator()))
174            .add(item);
175    }
176
177    /**
178     * Returns a sorted list of items whose names start with the
179     * provided character.
180     *
181     * @param key index key
182     * @return list of items keyed by the provided character
183     */
184    public SortedSet<IndexItem> getItems(Character key) {
185        return itemsByFirstChar.get(key);
186    }
187
188    /**
189     * Returns a sorted list of the first characters of the labels of index items.
190     */
191    public List<Character> getFirstCharacters() {
192        return new ArrayList<>(itemsByFirstChar.keySet());
193    }
194
195    /**
196     * Returns a sorted list of items in a given category.
197     *
198     * @param cat the category
199     * @return list of items keyed by the provided character
200     */
201    public SortedSet<IndexItem> getItems(IndexItem.Category cat) {
202        Objects.requireNonNull(cat);
203        return itemsByCategory.getOrDefault(cat, Collections.emptySortedSet());
204    }
205
206    /**
207     * Returns a sorted list of items with a given kind of doc tree.
208     *
209     * @param kind the kind
210     * @return list of items keyed by the provided character
211     */
212    public SortedSet<IndexItem> getItems(DocTree.Kind kind) {
213        Objects.requireNonNull(kind);
214        return itemsByCategory
215            .getOrDefault(IndexItem.Category.TAGS, Collections.emptySortedSet())
216            .stream()
217            .filter(i -> i.isKind(kind))
218            .collect(
219                Collectors.toCollection(() -> new TreeSet<>(mainComparator)));
220    }
221
222    /**
223     * Indexes all the members (fields, methods, constructors, etc.) of the
224     * provided type element.
225     *
226     * @param te TypeElement whose members are to be indexed
227     */
228    private void indexMembers(TypeElement te) {
229        VisibleMemberTable vmt = configuration.getVisibleMemberTable(te);
230        indexMembers(te, vmt.getVisibleMembers(FIELDS));
231        indexMembers(te,
232            vmt.getVisibleMembers(ANNOTATION_TYPE_MEMBER_OPTIONAL));
233        indexMembers(te,
234            vmt.getVisibleMembers(ANNOTATION_TYPE_MEMBER_REQUIRED));
235        indexMembers(te, vmt.getVisibleMembers(METHODS));
236        indexMembers(te, vmt.getVisibleMembers(CONSTRUCTORS));
237        indexMembers(te, vmt.getVisibleMembers(ENUM_CONSTANTS));
238    }
239
240    /**
241     * Indexes the provided elements.
242     *
243     * @param members a collection of elements
244     */
245    private void indexMembers(TypeElement typeElement,
246            Iterable<? extends Element> members) {
247        for (Element member : members) {
248            if (shouldIndex(member)) {
249                add(IndexItem.of(typeElement, member, utils));
250            }
251        }
252    }
253
254    /**
255     * Index the given type elements.
256     *
257     * @param elements type elements
258     */
259    private void indexTypeElements(Iterable<TypeElement> elements) {
260        for (TypeElement typeElement : elements) {
261            if (shouldIndex(typeElement)) {
262                add(IndexItem.of(typeElement, utils));
263            }
264        }
265    }
266
267    /**
268     * Indexes all the modules.
269     */
270    private void indexModules() {
271        for (ModuleElement m : configuration.modules) {
272            add(IndexItem.of(m, utils));
273        }
274    }
275
276    /**
277     * Index the given package element.
278     *
279     * @param packageElement the package element
280     */
281    private void indexPackage(PackageElement packageElement) {
282        if (shouldIndex(packageElement)) {
283            add(IndexItem.of(packageElement, utils));
284        }
285    }
286
287    /**
288     * Should this element be added to the index?
289     */
290    private boolean shouldIndex(Element element) {
291        if (utils.hasHiddenTag(element)) {
292            return false;
293        }
294
295        if (utils.isPackage(element)) {
296            // Do not add to index map if -nodeprecated option is set and the
297            // package is marked as deprecated.
298            return !(noDeprecated && utils.isDeprecated(element));
299        } else {
300            // Do not add to index map if -nodeprecated option is set and if the
301            // element is marked as deprecated or the containing package is
302            // marked as
303            // deprecated.
304            return !(noDeprecated &&
305                (utils.isDeprecated(element) ||
306                    utils.isDeprecated(utils.containingPackage(element))));
307        }
308    }
309
310    private static Character keyCharacter(String s) {
311        // Use first valid java identifier start character as key,
312        // or '*' for strings that do not contain one.
313        for (int i = 0; i < s.length(); i++) {
314            if (Character.isJavaIdentifierStart(s.charAt(i))) {
315                return Character.toUpperCase(s.charAt(i));
316            }
317        }
318        return '*';
319    }
320
321    /**
322     * Returns a comparator for the all-classes list.
323     * @return a comparator for class element items
324     */
325    private Comparator<IndexItem> makeClassComparator() {
326        return Comparator.comparing(IndexItem::getElement,
327            utils.comparators.makeAllClassesComparator());
328    }
329
330    /**
331     * Returns a comparator for the {@code IndexItem}s in the index page.
332     * This is a composite comparator that must be able to compare all kinds of items:
333     * for element items, tag items, and others.
334     *
335     * @return a comparator for index page items
336     */
337    private Comparator<IndexItem> makeIndexComparator() {
338        // We create comparators specific to element and search tag items, and a
339        // base comparator used to compare between the two kinds of items.
340        // In order to produce consistent results, it is important that the base
341        // comparator
342        // uses the same primary sort keys as both the element and search tag
343        // comparators
344        // (see JDK-8311264).
345        Comparator<Element> elementComparator
346            = utils.comparators.makeIndexElementComparator();
347        Comparator<IndexItem> baseComparator = (ii1, ii2) -> utils
348            .compareStrings(getIndexItemKey(ii1), getIndexItemKey(ii2));
349        Comparator<IndexItem> searchTagComparator = baseComparator
350            .thenComparing(IndexItem::getHolder)
351            .thenComparing(IndexItem::getDescription)
352            .thenComparing(IndexItem::getUrl);
353
354        return (ii1, ii2) -> {
355            // If both are element items, compare the elements
356            if (ii1.isElementItem() && ii2.isElementItem()) {
357                int d = elementComparator.compare(ii1.getElement(),
358                    ii2.getElement());
359                if (d == 0) {
360                    /*
361                     * Members inherited from classes with package access are
362                     * documented as though they were declared in the inheriting
363                     * subclass (see JDK-4780441).
364                     */
365                    Element subclass1 = ii1.getContainingTypeElement();
366                    Element subclass2 = ii2.getContainingTypeElement();
367                    if (subclass1 != null && subclass2 != null) {
368                        d = elementComparator.compare(subclass1, subclass2);
369                    }
370                }
371                return d;
372            }
373
374            // If one is an element item, compare item keys; if equal, put
375            // element item last
376            if (ii1.isElementItem() || ii2.isElementItem()) {
377                int d = baseComparator.compare(ii1, ii2);
378                return d != 0 ? d : ii1.isElementItem() ? 1 : -1;
379            }
380
381            // Otherwise, compare labels and other fields of the items
382            return searchTagComparator.compare(ii1, ii2);
383        };
384    }
385
386    private String getIndexItemKey(IndexItem ii) {
387        // For element items return the key used by the element comparator;
388        // for search tag items return the item's label.
389        return ii.isElementItem()
390            ? utils.comparators.getIndexElementKey(ii.getElement())
391            : ii.getLabel();
392    }
393
394    /**
395     * Returns a Comparator for IndexItems in the types category of the search index.
396     * Items are compared by short name, falling back to the main comparator if names are equal.
397     *
398     * @return a Comparator
399     */
400    public Comparator<IndexItem> makeTypeSearchIndexComparator() {
401        Comparator<IndexItem> simpleNameComparator = (ii1, ii2) -> utils
402            .compareStrings(ii1.getSimpleName(), ii2.getSimpleName());
403        return simpleNameComparator.thenComparing(mainComparator);
404    }
405
406    /**
407     * Returns a Comparator for IndexItems in the modules, packages, members, and search tags
408     * categories of the search index.
409     * Items are compared by label, falling back to the main comparator if names are equal.
410     *
411     * @return a Comparator
412     */
413    public Comparator<IndexItem> makeGenericSearchIndexComparator() {
414        Comparator<IndexItem> labelComparator = (ii1, ii2) -> utils
415            .compareStrings(ii1.getLabel(), ii2.getLabel());
416        return labelComparator.thenComparing(mainComparator);
417    }
418}