001/*
002 * Copyright (c) 1997, 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.Arrays;
030import java.util.List;
031import java.util.SortedSet;
032import java.util.function.Predicate;
033import java.util.regex.Pattern;
034import java.util.stream.Collectors;
035
036import javax.lang.model.element.Element;
037import javax.lang.model.element.ModuleElement;
038import javax.lang.model.element.PackageElement;
039import javax.lang.model.element.TypeElement;
040
041import org.jdrupes.mdoclet.internal.doclets.formats.html.Navigation.PageMode;
042import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.BodyContents;
043import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.ContentBuilder;
044import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Entity;
045import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlStyle;
046import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlTree;
047import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.TagName;
048import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Text;
049import org.jdrupes.mdoclet.internal.doclets.toolkit.Content;
050import org.jdrupes.mdoclet.internal.doclets.toolkit.PackageSummaryWriter;
051import org.jdrupes.mdoclet.internal.doclets.toolkit.util.CommentHelper;
052import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFileIOException;
053import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPath;
054import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPaths;
055
056import com.sun.source.doctree.DeprecatedTree;
057import com.sun.source.doctree.DocTree;
058
059/**
060 * Class to generate file for each package contents in the right-hand
061 * frame. This will list all the Class Kinds in the package. A click on any
062 * class-kind will update the frame with the clicked class-kind page.
063 */
064public class PackageWriterImpl extends HtmlDocletWriter
065        implements PackageSummaryWriter {
066
067    /**
068     * The package being documented.
069     */
070    protected PackageElement packageElement;
071
072    private List<PackageElement> relatedPackages;
073    private SortedSet<TypeElement> allClasses;
074
075    /**
076     * The HTML element for the section tag being written.
077     */
078    private final HtmlTree section
079        = HtmlTree.SECTION(HtmlStyle.packageDescription, new ContentBuilder());
080
081    private final BodyContents bodyContents = new BodyContents();
082
083    // Maximum number of subpackages and sibling packages to list in related
084    // packages table
085    private static final int MAX_SUBPACKAGES = 20;
086    private static final int MAX_SIBLING_PACKAGES = 5;
087
088    /**
089     * Constructor to construct PackageWriter object and to generate
090     * "package-summary.html" file in the respective package directory.
091     * For example for package "java.lang" this will generate file
092     * "package-summary.html" file in the "java/lang" directory. It will also
093     * create "java/lang" directory in the current or the destination directory
094     * if it doesn't exist.
095     *
096     * @param configuration the configuration of the doclet.
097     * @param packageElement    PackageElement under consideration.
098     */
099    public PackageWriterImpl(HtmlConfiguration configuration,
100            PackageElement packageElement) {
101        super(configuration,
102            configuration.docPaths.forPackage(packageElement)
103                .resolve(DocPaths.PACKAGE_SUMMARY));
104        this.packageElement = packageElement;
105        computePackageData();
106    }
107
108    @Override
109    public Content getPackageHeader() {
110        String packageName = getLocalizedPackageName(packageElement).toString();
111        HtmlTree body = getBody(getWindowTitle(packageName));
112        var div = HtmlTree.DIV(HtmlStyle.header);
113        if (configuration.showModules) {
114            ModuleElement mdle = configuration.docEnv.getElementUtils()
115                .getModuleOf(packageElement);
116            var classModuleLabel = HtmlTree.SPAN(HtmlStyle.moduleLabelInPackage,
117                contents.moduleLabel);
118            var moduleNameDiv
119                = HtmlTree.DIV(HtmlStyle.subTitle, classModuleLabel);
120            moduleNameDiv.add(Entity.NO_BREAK_SPACE);
121            moduleNameDiv.add(getModuleLink(mdle,
122                Text.of(mdle.getQualifiedName().toString())));
123            div.add(moduleNameDiv);
124        }
125        Content packageHead = new ContentBuilder();
126        if (!packageElement.isUnnamed()) {
127            packageHead.add(contents.packageLabel).add(" ");
128        }
129        packageHead.add(packageName);
130        var tHeading = HtmlTree.HEADING_TITLE(Headings.PAGE_TITLE_HEADING,
131            HtmlStyle.title, packageHead);
132        div.add(tHeading);
133        bodyContents.setHeader(getHeader(PageMode.PACKAGE, packageElement))
134            .addMainContent(div);
135        return body;
136    }
137
138    @Override
139    public Content getContentHeader() {
140        return new ContentBuilder();
141    }
142
143    private void computePackageData() {
144        relatedPackages = findRelatedPackages();
145        boolean isSpecified = utils.isSpecified(packageElement);
146        allClasses = filterClasses(isSpecified
147            ? utils.getAllClasses(packageElement)
148            : configuration.typeElementCatalog.allClasses(packageElement));
149    }
150
151    private SortedSet<TypeElement> filterClasses(SortedSet<TypeElement> types) {
152        List<TypeElement> typeList = types
153            .stream()
154            .filter(
155                te -> utils.isCoreClass(te) && configuration.isGeneratedDoc(te))
156            .collect(Collectors.toList());
157        return utils.filterOutPrivateClasses(typeList, options.javafx());
158    }
159
160    private List<PackageElement> findRelatedPackages() {
161        String pkgName = packageElement.getQualifiedName().toString();
162
163        // always add superpackage
164        int lastdot = pkgName.lastIndexOf('.');
165        String pkgPrefix = lastdot > 0 ? pkgName.substring(0, lastdot) : null;
166        List<PackageElement> packages = new ArrayList<>(
167            filterPackages(
168                p -> p.getQualifiedName().toString().equals(pkgPrefix)));
169        boolean hasSuperPackage = !packages.isEmpty();
170
171        // add subpackages unless there are very many of them
172        Pattern subPattern
173            = Pattern.compile(pkgName.replace(".", "\\.") + "\\.\\w+");
174        List<PackageElement> subpackages = filterPackages(
175            p -> subPattern.matcher(p.getQualifiedName().toString()).matches());
176        if (subpackages.size() <= MAX_SUBPACKAGES) {
177            packages.addAll(subpackages);
178        }
179
180        // only add sibling packages if there is a non-empty superpackage, we
181        // are beneath threshold,
182        // and number of siblings is beneath threshold as well
183        if (hasSuperPackage && pkgPrefix != null
184            && packages.size() <= MAX_SIBLING_PACKAGES) {
185            Pattern siblingPattern
186                = Pattern.compile(pkgPrefix.replace(".", "\\.") + "\\.\\w+");
187
188            List<PackageElement> siblings = filterPackages(
189                p -> siblingPattern.matcher(p.getQualifiedName().toString())
190                    .matches());
191            if (siblings.size() <= MAX_SIBLING_PACKAGES) {
192                packages.addAll(siblings);
193            }
194        }
195        return packages;
196    }
197
198    @Override
199    protected Navigation getNavBar(PageMode pageMode, Element element) {
200        Content linkContent
201            = getModuleLink(utils.elementUtils.getModuleOf(packageElement),
202                contents.moduleLabel);
203        return super.getNavBar(pageMode, element)
204            .setNavLinkModule(linkContent)
205            .setSubNavLinks(() -> List.of(
206                links.createLink(HtmlIds.PACKAGE_DESCRIPTION,
207                    contents.navDescription,
208                    !utils.getFullBody(packageElement).isEmpty()
209                        && !options.noComment()),
210                links.createLink(HtmlIds.RELATED_PACKAGE_SUMMARY,
211                    contents.relatedPackages,
212                    relatedPackages != null && !relatedPackages.isEmpty()),
213                links.createLink(HtmlIds.CLASS_SUMMARY,
214                    contents.navClassesAndInterfaces,
215                    allClasses != null && !allClasses.isEmpty())));
216    }
217
218    /**
219     * Add the package deprecation information to the documentation tree.
220     *
221     * @param div the content to which the deprecation information will be added
222     */
223    public void addDeprecationInfo(Content div) {
224        List<? extends DeprecatedTree> deprs
225            = utils.getDeprecatedTrees(packageElement);
226        if (utils.isDeprecated(packageElement)) {
227            CommentHelper ch = utils.getCommentHelper(packageElement);
228            var deprDiv = HtmlTree.DIV(HtmlStyle.deprecationBlock);
229            var deprPhrase = HtmlTree.SPAN(HtmlStyle.deprecatedLabel,
230                getDeprecatedPhrase(packageElement));
231            deprDiv.add(deprPhrase);
232            if (!deprs.isEmpty()) {
233                List<? extends DocTree> commentTags
234                    = ch.getDescription(deprs.get(0));
235                if (!commentTags.isEmpty()) {
236                    addInlineDeprecatedComment(packageElement, deprs.get(0),
237                        deprDiv);
238                }
239            }
240            div.add(deprDiv);
241        }
242    }
243
244    @Override
245    public Content getSummariesList() {
246        return HtmlTree.UL(HtmlStyle.summaryList);
247    }
248
249    @Override
250    public void addRelatedPackagesSummary(Content summaryContent) {
251        boolean showModules = configuration.showModules
252            && hasRelatedPackagesInOtherModules(relatedPackages);
253        TableHeader tableHeader = showModules
254            ? new TableHeader(contents.moduleLabel, contents.packageLabel,
255                contents.descriptionLabel)
256            : new TableHeader(contents.packageLabel, contents.descriptionLabel);
257        addPackageSummary(relatedPackages, contents.relatedPackages,
258            tableHeader,
259            summaryContent, showModules);
260    }
261
262    /**
263     * Add all types to the content.
264     *
265     * @param target the content to which the links will be added
266     */
267    public void addAllClassesAndInterfacesSummary(Content target) {
268        var table = new Table<TypeElement>(HtmlStyle.summaryTable)
269            .setHeader(
270                new TableHeader(contents.classLabel, contents.descriptionLabel))
271            .setColumnStyles(HtmlStyle.colFirst, HtmlStyle.colLast)
272            .setId(HtmlIds.CLASS_SUMMARY)
273            .setDefaultTab(contents.allClassesAndInterfacesLabel)
274            .addTab(contents.interfaces, utils::isPlainInterface)
275            .addTab(contents.classes, e -> utils.isNonThrowableClass(e))
276            .addTab(contents.enums, utils::isEnum)
277            .addTab(contents.records, e -> utils.isRecord(e))
278            .addTab(contents.exceptionClasses, e -> utils.isThrowable(e))
279            .addTab(contents.annotationTypes, utils::isAnnotationInterface);
280        for (TypeElement typeElement : allClasses) {
281            if (typeElement != null && utils.isCoreClass(typeElement)) {
282                Content classLink = getLink(new HtmlLinkInfo(
283                    configuration,
284                    HtmlLinkInfo.Kind.SHOW_TYPE_PARAMS_AND_BOUNDS,
285                    typeElement));
286                ContentBuilder description = new ContentBuilder();
287                addPreviewSummary(typeElement, description);
288                if (utils.isDeprecated(typeElement)) {
289                    description.add(getDeprecatedPhrase(typeElement));
290                    List<? extends DeprecatedTree> tags
291                        = utils.getDeprecatedTrees(typeElement);
292                    if (!tags.isEmpty()) {
293                        addSummaryDeprecatedComment(typeElement, tags.get(0),
294                            description);
295                    }
296                } else {
297                    addSummaryComment(typeElement, description);
298                }
299                table.addRow(typeElement,
300                    Arrays.asList(classLink, description));
301            }
302        }
303        if (!table.isEmpty()) {
304            target.add(HtmlTree.LI(table));
305        }
306    }
307
308    public void addPackageSummary(List<PackageElement> packages, Content label,
309            TableHeader tableHeader, Content summaryContent,
310            boolean showModules) {
311        if (!packages.isEmpty()) {
312            var table = new Table<Void>(HtmlStyle.summaryTable)
313                .setId(HtmlIds.RELATED_PACKAGE_SUMMARY)
314                .setCaption(label)
315                .setHeader(tableHeader);
316            if (showModules) {
317                table.setColumnStyles(HtmlStyle.colPlain, HtmlStyle.colFirst,
318                    HtmlStyle.colLast);
319            } else {
320                table.setColumnStyles(HtmlStyle.colFirst, HtmlStyle.colLast);
321            }
322
323            for (PackageElement pkg : packages) {
324                Content packageLink
325                    = getPackageLink(pkg, Text.of(pkg.getQualifiedName()));
326                Content moduleLink = Text.EMPTY;
327                if (showModules) {
328                    ModuleElement module
329                        = (ModuleElement) pkg.getEnclosingElement();
330                    if (module != null && !module.isUnnamed()) {
331                        moduleLink = getModuleLink(module,
332                            Text.of(module.getQualifiedName()));
333                    }
334                }
335                ContentBuilder description = new ContentBuilder();
336                addPreviewSummary(pkg, description);
337                if (utils.isDeprecated(pkg)) {
338                    description.add(getDeprecatedPhrase(pkg));
339                    List<? extends DeprecatedTree> tags
340                        = utils.getDeprecatedTrees(pkg);
341                    if (!tags.isEmpty()) {
342                        addSummaryDeprecatedComment(pkg, tags.get(0),
343                            description);
344                    }
345                } else {
346                    addSummaryComment(pkg, description);
347                }
348                if (showModules) {
349                    table.addRow(moduleLink, packageLink, description);
350                } else {
351                    table.addRow(packageLink, description);
352                }
353            }
354            summaryContent.add(HtmlTree.LI(table));
355        }
356    }
357
358    @Override
359    public void addPackageDescription(Content packageContent) {
360        addPreviewInfo(packageElement, packageContent);
361        if (!utils.getBody(packageElement).isEmpty()) {
362            section.setId(HtmlIds.PACKAGE_DESCRIPTION);
363            addDeprecationInfo(section);
364            addInlineComment(packageElement, section);
365        }
366    }
367
368    @Override
369    public void addPackageTags(Content packageContent) {
370        addTagsInfo(packageElement, section);
371        packageContent.add(section);
372    }
373
374    @Override
375    public void addPackageSignature(Content packageContent) {
376        packageContent.add(new HtmlTree(TagName.HR));
377        packageContent
378            .add(Signatures.getPackageSignature(packageElement, this));
379    }
380
381    @Override
382    public void addPackageContent(Content packageContent) {
383        bodyContents.addMainContent(packageContent);
384    }
385
386    @Override
387    public void addPackageFooter() {
388        bodyContents.setFooter(getFooter());
389    }
390
391    @Override
392    public void printDocument(Content content) throws DocFileIOException {
393        String description = getDescription("declaration", packageElement);
394        List<DocPath> localStylesheets = getLocalStylesheets(packageElement);
395        content.add(bodyContents);
396        printHtmlDocument(
397            configuration.metakeywords.getMetaKeywords(packageElement),
398            description, localStylesheets, content);
399    }
400
401    @Override
402    public Content getPackageSummary(Content summaryContent) {
403        return HtmlTree.SECTION(HtmlStyle.summary, summaryContent);
404    }
405
406    private boolean hasRelatedPackagesInOtherModules(
407            List<PackageElement> relatedPackages) {
408        final ModuleElement module
409            = (ModuleElement) packageElement.getEnclosingElement();
410        return relatedPackages.stream()
411            .anyMatch(pkg -> module != pkg.getEnclosingElement());
412    }
413
414    private List<PackageElement>
415            filterPackages(Predicate<? super PackageElement> filter) {
416        return configuration.packages.stream()
417            .filter(p -> p != packageElement && filter.test(p))
418            .collect(Collectors.toList());
419    }
420}