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}