001/*
002 * Copyright (c) 2019, 2022, 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.net.URI;
029import java.net.URISyntaxException;
030import java.nio.file.Path;
031import java.text.Collator;
032import java.util.ArrayList;
033import java.util.Comparator;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.TreeMap;
038import java.util.TreeSet;
039import java.util.WeakHashMap;
040import java.util.function.Predicate;
041
042import javax.lang.model.element.Element;
043import javax.tools.Diagnostic;
044
045import org.jdrupes.mdoclet.internal.doclets.formats.html.Navigation.PageMode;
046import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.BodyContents;
047import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.ContentBuilder;
048import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlStyle;
049import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlTree;
050import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Text;
051import org.jdrupes.mdoclet.internal.doclets.toolkit.Content;
052import org.jdrupes.mdoclet.internal.doclets.toolkit.DocletElement;
053import org.jdrupes.mdoclet.internal.doclets.toolkit.OverviewElement;
054import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFileIOException;
055import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPath;
056import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPaths;
057import org.jdrupes.mdoclet.internal.doclets.toolkit.util.IndexItem;
058
059import com.sun.source.doctree.DocTree;
060import com.sun.source.doctree.SpecTree;
061import com.sun.source.util.DocTreePath;
062import com.sun.source.util.TreePath;
063
064import static java.util.stream.Collectors.groupingBy;
065import static java.util.stream.Collectors.toList;
066
067/**
068 * Generates the file with the summary of all the references to external specifications.
069 */
070public class ExternalSpecsWriter extends HtmlDocletWriter {
071
072    private final Navigation navBar;
073
074    /**
075     * Cached contents of {@code <title>...</title>} tags of the HTML pages.
076     */
077    final Map<Element, String> titles = new WeakHashMap<>();
078
079    /**
080     * Constructs ExternalSpecsWriter object.
081     *
082     * @param configuration The current configuration
083     * @param filename Path to the file which is getting generated.
084     */
085    public ExternalSpecsWriter(HtmlConfiguration configuration,
086            DocPath filename) {
087        super(configuration, filename);
088        this.navBar = new Navigation(null, configuration,
089            PageMode.EXTERNAL_SPECS, path);
090    }
091
092    public static void generate(HtmlConfiguration configuration)
093            throws DocFileIOException {
094        generate(configuration, DocPaths.EXTERNAL_SPECS);
095    }
096
097    private static void generate(HtmlConfiguration configuration,
098            DocPath fileName) throws DocFileIOException {
099        boolean hasExternalSpecs = configuration.mainIndex != null
100            && !configuration.mainIndex.getItems(DocTree.Kind.SPEC).isEmpty();
101        if (!hasExternalSpecs) {
102            return;
103        }
104        ExternalSpecsWriter w
105            = new ExternalSpecsWriter(configuration, fileName);
106        w.buildExternalSpecsPage();
107        configuration.conditionalPages
108            .add(HtmlConfiguration.ConditionalPage.EXTERNAL_SPECS);
109    }
110
111    /**
112     * Prints all the "external specs" to the file.
113     */
114    protected void buildExternalSpecsPage() throws DocFileIOException {
115        checkUniqueItems();
116
117        String title = resources.getText("doclet.External_Specifications");
118        HtmlTree body = getBody(getWindowTitle(title));
119        Content mainContent = new ContentBuilder();
120        addExternalSpecs(mainContent);
121        body.add(new BodyContents()
122            .setHeader(getHeader(PageMode.EXTERNAL_SPECS))
123            .addMainContent(HtmlTree.DIV(HtmlStyle.header,
124                HtmlTree.HEADING(Headings.PAGE_TITLE_HEADING,
125                    contents.getContent("doclet.External_Specifications"))))
126            .addMainContent(mainContent)
127            .setFooter(getFooter()));
128        printHtmlDocument(null, "external specifications", body);
129
130        if (configuration.mainIndex != null) {
131            configuration.mainIndex
132                .add(IndexItem.of(IndexItem.Category.TAGS, title, path));
133        }
134    }
135
136    protected void checkUniqueItems() {
137        Map<String, Map<String, List<IndexItem>>> itemsByURL = new HashMap<>();
138        Map<String, Map<String, List<IndexItem>>> itemsByTitle
139            = new HashMap<>();
140        for (IndexItem ii : configuration.mainIndex
141            .getItems(DocTree.Kind.SPEC)) {
142            if (ii.getDocTree() instanceof SpecTree st) {
143                String url = st.getURL().toString();
144                String title = ii.getLabel(); // normalized form of
145                                              // st.getTitle()
146                itemsByTitle
147                    .computeIfAbsent(title, l -> new HashMap<>())
148                    .computeIfAbsent(url, u -> new ArrayList<>())
149                    .add(ii);
150                itemsByURL
151                    .computeIfAbsent(url, u -> new HashMap<>())
152                    .computeIfAbsent(title, l -> new ArrayList<>())
153                    .add(ii);
154            }
155        }
156
157        itemsByURL.forEach((url, title) -> {
158            if (title.size() > 1) {
159                messages.error("doclet.extSpec.spec.has.multiple.titles", url,
160                    title.values().stream().distinct().count());
161                title.forEach((t, list) -> list.forEach(
162                    ii -> report(ii, "doclet.extSpec.url.title", url, t)));
163            }
164        });
165
166        itemsByTitle.forEach((title, urls) -> {
167            if (urls.size() > 1) {
168                messages.error("doclet.extSpec.title.for.multiple.specs", title,
169                    urls.values().stream().distinct().count());
170                urls.forEach((u, list) -> list.forEach(
171                    ii -> report(ii, "doclet.extSpec.title.url", title, u)));
172            }
173        });
174    }
175
176    private void report(IndexItem ii, String key, Object... args) {
177        String message = messages.getResources().getText(key, args);
178        Element e = ii.getElement();
179        if (e == null) {
180            configuration.reporter.print(Diagnostic.Kind.NOTE, message);
181        } else {
182            TreePath tp = utils.getTreePath(e);
183            DocTreePath dtp = new DocTreePath(
184                new DocTreePath(tp, utils.getDocCommentTree(e)),
185                ii.getDocTree());
186            configuration.reporter.print(Diagnostic.Kind.NOTE, dtp, message);
187        }
188    }
189
190    /**
191     * Adds all the references to external specifications to the content tree.
192     *
193     * @param content HtmlTree content to which the links will be added
194     */
195    protected void addExternalSpecs(Content content) {
196        final int USE_DETAILS_THRESHHOLD = 20;
197        Map<String, List<IndexItem>> searchIndexMap = groupExternalSpecs();
198
199        var hostNamesSet = new TreeSet<String>();
200        boolean noHost = false;
201        for (var searchIndexItems : searchIndexMap.values()) {
202            try {
203                URI uri = getSpecURI(searchIndexItems.get(0));
204                String host = uri.getHost();
205                if (host != null) {
206                    hostNamesSet.add(host);
207                } else {
208                    noHost = true;
209                }
210            } catch (URISyntaxException e) {
211                // ignore
212            }
213        }
214        var hostNamesList = new ArrayList<>(hostNamesSet);
215
216        var table = new Table<URI>(HtmlStyle.summaryTable)
217            .setCaption(contents.externalSpecifications)
218            .setHeader(new TableHeader(contents.specificationLabel,
219                contents.referencedIn))
220            .setColumnStyles(HtmlStyle.colFirst, HtmlStyle.colLast)
221            .setId(HtmlIds.EXTERNAL_SPECS);
222        if ((hostNamesList.size() + (noHost ? 1 : 0)) > 1) {
223            for (var host : hostNamesList) {
224                table.addTab(Text.of(host), u -> host.equals(u.getHost()));
225            }
226            if (noHost) {
227                table.addTab(
228                    Text.of(resources
229                        .getText("doclet.External_Specifications.no-host")),
230                    u -> u.getHost() == null);
231            }
232        }
233        table.setDefaultTab(Text.of(resources
234            .getText("doclet.External_Specifications.All_Specifications")));
235
236        for (List<IndexItem> searchIndexItems : searchIndexMap.values()) {
237            IndexItem ii = searchIndexItems.get(0);
238            Content specName = createSpecLink(ii);
239            Content referencesList
240                = HtmlTree.UL(HtmlStyle.refList, searchIndexItems,
241                    item -> HtmlTree.LI(createLink(item)));
242            Content references
243                = searchIndexItems.size() < USE_DETAILS_THRESHHOLD
244                    ? referencesList
245                    : HtmlTree.DETAILS()
246                        .add(HtmlTree
247                            .SUMMARY(contents.getContent("doclet.references",
248                                String.valueOf(searchIndexItems.size()))))
249                        .add(referencesList);
250            try {
251                URI uri = getSpecURI(ii);
252                table.addRow(uri, specName, references);
253            } catch (URISyntaxException e) {
254                table.addRow(specName, references);
255            }
256        }
257        content.add(table);
258    }
259
260    private Map<String, List<IndexItem>> groupExternalSpecs() {
261        return configuration.mainIndex.getItems(DocTree.Kind.SPEC).stream()
262            .collect(groupingBy(IndexItem::getLabel,
263                () -> new TreeMap<>(getTitleComparator()), toList()));
264    }
265
266    Comparator<String> getTitleComparator() {
267        Collator collator = Collator.getInstance();
268        return new Comparator<>() {
269            @Override
270            public int compare(String s1, String s2) {
271                int i1 = 0;
272                int i2 = 0;
273                while (i1 < s1.length() && i2 < s2.length()) {
274                    int j1 = find(s1, i1, Character::isDigit);
275                    int j2 = find(s2, i2, Character::isDigit);
276                    int cmp = collator.compare(s1.substring(i1, j1),
277                        s2.substring(i2, j2));
278                    if (cmp != 0) {
279                        return cmp;
280                    }
281                    if (j1 == s1.length() || j2 == s2.length()) {
282                        i1 = j1;
283                        i2 = j2;
284                        break;
285                    }
286                    int k1 = find(s1, j1, ch -> !Character.isDigit(ch));
287                    int k2 = find(s2, j2, ch -> !Character.isDigit(ch));
288                    cmp = Integer.compare(
289                        Integer.parseInt(s1.substring(j1, k1)),
290                        Integer.parseInt(s2.substring(j2, k2)));
291                    if (cmp != 0) {
292                        return cmp;
293                    }
294                    i1 = k1;
295                    i2 = k2;
296                }
297                return i1 < s1.length() ? 1 : i2 < s2.length() ? -1 : 0;
298            }
299        };
300    }
301
302    private static int find(String s, int start, Predicate<Character> p) {
303        int i = start;
304        while (i < s.length() && !p.test(s.charAt(i))) {
305            i++;
306        }
307        return i;
308    }
309
310    private Content createLink(IndexItem i) {
311        assert i.getDocTree().getKind() == DocTree.Kind.SPEC : i;
312        Element element = i.getElement();
313        if (element instanceof OverviewElement) {
314            return links.createLink(pathToRoot.resolve(i.getUrl()),
315                resources.getText("doclet.Overview"));
316        } else if (element instanceof DocletElement) {
317            DocletElement e = (DocletElement) element;
318            // Implementations of DocletElement do not override equals and
319            // hashCode; putting instances of DocletElement in a map is not
320            // incorrect, but might well be inefficient
321            String t = titles.computeIfAbsent(element, utils::getHTMLTitle);
322            if (t.isBlank()) {
323                // The user should probably be notified (a warning?) that this
324                // file does not have a title
325                Path p = Path.of(e.getFileObject().toUri());
326                t = p.getFileName().toString();
327            }
328            ContentBuilder b = new ContentBuilder();
329            b.add(HtmlTree.CODE(Text.of(i.getHolder() + ": ")));
330            // non-program elements should be displayed using a normal font
331            b.add(t);
332            return links.createLink(pathToRoot.resolve(i.getUrl()), b);
333        } else {
334            // program elements should be displayed using a code font
335            Content link = links.createLink(pathToRoot.resolve(i.getUrl()),
336                i.getHolder());
337            return HtmlTree.CODE(link);
338        }
339    }
340
341    /**
342     * {@return the fully-resolved URI in index item for a {@code @spec} tag}
343     *
344     * While the signature declares that it may throw {@code URISynaxException},
345     * that should not occur: items with bad URIs should not make it into the index.
346     *
347     * @param i the index item
348     * @throws URISyntaxException if there is an issue creating the URI
349     */
350    private URI getSpecURI(IndexItem i) throws URISyntaxException {
351        assert i.getDocTree().getKind() == DocTree.Kind.SPEC : i;
352        SpecTree specTree = (SpecTree) i.getDocTree();
353
354        URI specURI = new URI(specTree.getURL().getBody());
355        return resolveExternalSpecURI(specURI);
356    }
357
358    private Content createSpecLink(IndexItem i) {
359        Content title = Text.of(i.getLabel());
360        try {
361            URI uri = getSpecURI(i);
362            return HtmlTree.A(uri, title);
363        } catch (URISyntaxException e) {
364            // should not happen: items with bad URIs should not make it into
365            // the index
366            return title;
367        }
368    }
369}