1 /*
2  * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package build.tools.jigsaw;
27 
28 import java.io.IOException;
29 import java.io.PrintStream;
30 import java.lang.module.Configuration;
31 import java.lang.module.ModuleDescriptor;
32 import java.lang.module.ModuleFinder;
33 import java.lang.module.ModuleReference;
34 import java.lang.module.ResolvedModule;
35 import java.nio.file.Files;
36 import java.nio.file.Path;
37 import java.nio.file.Paths;
38 import java.util.Arrays;
39 import java.util.Comparator;
40 import java.util.Date;
41 import java.util.Enumeration;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.stream.Collectors;
47 import java.util.zip.ZipEntry;
48 import java.util.zip.ZipFile;
49 import static java.lang.module.ModuleDescriptor.*;
50 import static build.tools.jigsaw.ModuleSummary.HtmlDocument.Selector.*;
51 import static build.tools.jigsaw.ModuleSummary.HtmlDocument.Division.*;
52 
53 public class ModuleSummary {
54     private static final String USAGE = "Usage: ModuleSummary --module-path <dir> -o <outfile> [--root mn]*";
55 
main(String[] args)56     public static void main(String[] args) throws Exception {
57         int i=0;
58         Path modpath = null;
59         Path outfile = null;
60         Set<String> roots = new HashSet<>();
61         while (i < args.length && args[i].startsWith("-")) {
62             String arg = args[i++];
63             switch (arg) {
64                 case "--module-path":
65                     modpath = Paths.get(args[i++]);
66                     break;
67                 case "-o":
68                     outfile = Paths.get(args[i++]);
69                     break;
70                 case "--root":
71                     roots.add(args[i++]);
72                 default:
73                     System.err.println(USAGE);
74                     System.exit(-1);
75             }
76         }
77         if (outfile == null || modpath == null) {
78             System.err.println(USAGE);
79             System.exit(1);
80         }
81         Path dir = outfile.getParent() != null ? outfile.getParent() : Paths.get(".");
82         Files.createDirectories(dir);
83 
84         Map<String, ModuleSummary> modules = new HashMap<>();
85         Set<ModuleReference> mrefs = ModuleFinder.ofSystem().findAll();
86         for (ModuleReference mref : mrefs) {
87             String mn = mref.descriptor().name();
88             Path jmod = modpath.resolve(mn + ".jmod");
89             modules.put(mn, new ModuleSummary(mref, jmod));
90         }
91 
92         if (roots.isEmpty()) {
93             roots.addAll(modules.keySet());
94         }
95         genReport(outfile, modules, roots, "JDK Module Summary");
96     }
97 
genReport(Path outfile, Map<String, ModuleSummary> modules, Set<String> roots, String title)98     static void genReport(Path outfile, Map<String, ModuleSummary> modules, Set<String> roots, String title)
99         throws IOException
100     {
101         Configuration cf = resolve(roots);
102         try (PrintStream out = new PrintStream(Files.newOutputStream(outfile))) {
103             HtmlDocument doc = new HtmlDocument(title, modules);
104             Set<ModuleDescriptor> descriptors = cf.modules().stream()
105                     .map(ResolvedModule::reference)
106                     .map(ModuleReference::descriptor)
107                     .collect(Collectors.toSet());
108             doc.writeTo(out, descriptors);
109         }
110     }
111 
112     private final String name;
113     private final ModuleDescriptor descriptor;
114     private final JmodInfo jmodInfo;
ModuleSummary(ModuleReference mref, Path jmod)115     ModuleSummary(ModuleReference mref, Path jmod) throws IOException {
116         this.name = mref.descriptor().name();
117         this.descriptor = mref.descriptor();
118         this.jmodInfo = new JmodInfo(jmod);
119     }
120 
name()121     String name() {
122         return name;
123     }
124 
uncompressedSize()125     long uncompressedSize() {
126         return jmodInfo.size;
127     }
128 
jmodFileSize()129     long jmodFileSize() {
130         return jmodInfo.filesize; // estimated compressed size
131     }
132 
descriptor()133     ModuleDescriptor descriptor() {
134         return descriptor;
135     }
136 
numClasses()137     int numClasses() {
138         return jmodInfo.classCount;
139     }
140 
classBytes()141     long classBytes() {
142         return jmodInfo.classBytes;
143     }
144 
numResources()145     int numResources() {
146         return jmodInfo.resourceCount;
147     }
148 
resourceBytes()149     long resourceBytes() {
150         return jmodInfo.resourceBytes;
151     }
152 
numConfigs()153     int numConfigs() {
154         return jmodInfo.configCount;
155     }
configBytes()156     long configBytes() {
157         return jmodInfo.configBytes;
158     }
numCommands()159     int numCommands() {
160         return jmodInfo.nativeCmds.size();
161     }
162 
commandBytes()163     long commandBytes() {
164         return jmodInfo.nativeCmds.values().stream()
165                 .mapToLong(l -> l.longValue()).sum() - jmodInfo.debugInfoCmdBytes;
166     }
numCommandsDebug()167     int numCommandsDebug() {
168         return jmodInfo.debugInfoCmdCount;
169     }
commandDebugBytes()170     long commandDebugBytes() {
171         return jmodInfo.debugInfoCmdBytes;
172     }
numNativeLibraries()173     int numNativeLibraries() {
174         return jmodInfo.nativeLibs.size();
175     }
176 
nativeLibrariesBytes()177     long nativeLibrariesBytes() {
178         return jmodInfo.nativeLibs.values().stream()
179                 .mapToLong(l -> l.longValue()).sum() - jmodInfo.debugInfoLibBytes;
180     }
numNativeLibrariesDebug()181     int numNativeLibrariesDebug() {
182         return jmodInfo.debugInfoLibCount;
183     }
184 
nativeLibrariesDebugBytes()185     long nativeLibrariesDebugBytes() {
186         return jmodInfo.debugInfoLibBytes;
187     }
188 
commands()189     Map<String,Long> commands() {
190         return jmodInfo.nativeCmds;
191     }
192 
nativeLibs()193     Map<String,Long> nativeLibs() {
194         return jmodInfo.nativeLibs;
195     }
196 
configFiles()197     Map<String,Long> configFiles() {
198         return jmodInfo.configFiles;
199     }
200 
201 
202     static class JmodInfo {
203         final long size;
204         final long filesize;
205         final int  classCount;
206         final long classBytes;
207         final int  resourceCount;
208         final long resourceBytes;
209         final int  configCount;
210         final long configBytes;
211         final int  debugInfoLibCount;
212         final long debugInfoLibBytes;
213         final int  debugInfoCmdCount;
214         final long debugInfoCmdBytes;
215         final Map<String,Long> configFiles = new HashMap<>();
216         final Map<String,Long> nativeCmds = new HashMap<>();
217         final Map<String,Long> nativeLibs = new HashMap<>();
218 
JmodInfo(Path jmod)219         JmodInfo(Path jmod) throws IOException {
220             long total = 0;
221             long cBytes = 0, rBytes = 0, cfBytes = 0, dizLibBytes = 0, dizCmdBytes = 0;
222             int  cCount = 0, rCount = 0, cfCount = 0, dizLibCount = 0, dizCmdCount = 0;
223             try (ZipFile zf = new ZipFile(jmod.toFile())) {
224                 for (Enumeration<? extends ZipEntry> e = zf.entries(); e.hasMoreElements(); ) {
225                     ZipEntry ze = e.nextElement();
226                     String fn = ze.getName();
227                     int pos = fn.indexOf('/');
228                     String dir = fn.substring(0, pos);
229                     String filename = fn.substring(fn.lastIndexOf('/') + 1);
230                     // name shown in the column
231                     String name = filename;
232 
233                     long len = ze.getSize();
234                     total += len;
235                     switch (dir) {
236                         case NATIVE_LIBS:
237                             nativeLibs.put(name, len);
238                             if (filename.endsWith(".diz")) {
239                                 dizLibCount++;
240                                 dizLibBytes += len;
241                             }
242                             break;
243                         case NATIVE_CMDS:
244                             nativeCmds.put(name, len);
245                             if (filename.endsWith(".diz")) {
246                                 dizCmdCount++;
247                                 dizCmdBytes += len;
248                             }
249                             break;
250                         case CLASSES:
251                             if (filename.endsWith(".class")) {
252                                 cCount++;
253                                 cBytes += len;
254                             } else {
255                                 rCount++;
256                                 rBytes += len;
257                             }
258                             break;
259                         case CONFIG:
260                             configFiles.put(name, len);
261                             cfCount++;
262                             cfBytes += len;
263                             break;
264                         default:
265                             break;
266                     }
267                 }
268                 this.filesize = jmod.toFile().length();
269                 this.classCount = cCount;
270                 this.classBytes = cBytes;
271                 this.resourceCount = rCount;
272                 this.resourceBytes = rBytes;
273                 this.configCount = cfCount;
274                 this.configBytes = cfBytes;
275                 this.size = total;
276                 this.debugInfoLibCount = dizLibCount;
277                 this.debugInfoLibBytes = dizLibBytes;
278                 this.debugInfoCmdCount = dizCmdCount;
279                 this.debugInfoCmdBytes = dizCmdBytes;
280             }
281         }
282 
283         static final String NATIVE_LIBS = "native";
284         static final String NATIVE_CMDS = "bin";
285         static final String CLASSES     = "classes";
286         static final String CONFIG      = "conf";
287 
288         static final String MODULE_ID = "module/id";
289         static final String MODULE_MAIN_CLASS = "module/main-class";
290     }
291 
resolve(Set<String> roots)292     static Configuration resolve(Set<String> roots) {
293         return Configuration.empty()
294             .resolve(ModuleFinder.ofSystem(),
295                      ModuleFinder.of(),
296                      roots);
297     }
298 
299     static class HtmlDocument {
300         final String title;
301         final Map<String, ModuleSummary> modules;
302         boolean requiresTransitiveNote = false;
303         boolean aggregatorNote = false;
304         boolean totalBytesNote = false;
HtmlDocument(String title, Map<String, ModuleSummary> modules)305         HtmlDocument(String title, Map<String, ModuleSummary> modules) {
306             this.title = title;
307             this.modules = modules;
308         }
309 
writeTo(PrintStream out, Set<ModuleDescriptor> selectedModules)310         void writeTo(PrintStream out, Set<ModuleDescriptor> selectedModules) {
311             out.format("<html><head>%n");
312             out.format("<title>%s</title>%n", title);
313             // stylesheet
314             Arrays.stream(HtmlDocument.STYLES).forEach(out::println);
315             out.format("</head>%n");
316 
317             // body begins
318             out.format("<body>%n");
319 
320             // title and date
321             out.println(DOCTITLE.toString(title));
322             out.println(VERSION.toString(String.format("%tc", new Date())));
323 
324             // total modules and sizes
325             long totalBytes = selectedModules.stream()
326                     .map(ModuleDescriptor::name)
327                     .map(modules::get)
328                     .mapToLong(ModuleSummary::uncompressedSize)
329                     .sum();
330             String[] sections = new String[] {
331                     String.format("%s: %d", "Total modules", selectedModules.size()),
332                     String.format("%s: %,d bytes (%s %s)", "Total size",
333                                   totalBytes,
334                                   System.getProperty("os.name"),
335                                   System.getProperty("os.arch"))
336             };
337             out.println(SECTION.toString(sections));
338 
339             // write table and header
340             out.println(String.format("<table class=\"%s\">", MODULES));
341             out.println(header("Module", "Requires", "Exports",
342                     "Services", "Commands/Native Libraries/Configs"));
343 
344             // write contents - one row per module
345             selectedModules.stream()
346                     .sorted(Comparator.comparing(ModuleDescriptor::name))
347                     .map(m -> modules.get(m.name()))
348                     .map(ModuleTableRow::new)
349                     .forEach(table -> table.writeTo(out));
350 
351             out.format("</table>");  // end table
352             out.format("</body>");
353             out.println("</html>");
354         }
355 
header(String... columns)356         String header(String... columns) {
357             StringBuilder sb = new StringBuilder();
358             sb.append("<tr>");
359             Arrays.stream(columns)
360                     .forEach(cn -> sb.append("  <th>").append(cn).append("</th>").append("\n"));
361             sb.append("</tr>");
362             return sb.toString();
363         }
364 
365         static enum Selector {
366             MODULES("modules"),
367             MODULE("module"),
368             MODULE_DEF("code name def"),
369             AGGREGATOR("code name def agg"),
370             REQUIRES("code"),
371             REQUIRES_PUBLIC("code reexp"),
372             BR("br"),
373             CODE("code"),
374             NUMBER("number"),;
375             final String name;
Selector(String name)376             Selector(String name) {
377                 this.name = name;
378             }
379             @Override
toString()380             public String toString() {
381                 return name;
382             }
383         }
384 
385         static enum Division {
386             DOCTITLE("doctitle"),
387             VERSION("versions"),
388             SECTION("section");
389             final String name;
390 
Division(String name)391             Division(String name) {
392                 this.name = name;
393             }
394 
toString(String... lines)395             public String toString(String... lines) {
396                 String value = Arrays.stream(lines).collect(Collectors.joining("<br>\n"));
397                 return "<div class=\"" + name + "\">" + value + "</div>";
398             }
399         }
400 
401         class ModuleTableRow {
402             private final ModuleSummary ms;
403             private final Set<ModuleDescriptor> deps;
404             private final int maxRows;
405             private final boolean aggregator;
ModuleTableRow(ModuleSummary ms)406             ModuleTableRow(ModuleSummary ms) {
407                 this.ms = ms;
408                 Configuration cf = resolve(Set.of(ms.name()));
409                 this.deps = cf.modules().stream()
410                         .map(ResolvedModule::reference)
411                         .map(ModuleReference::descriptor)
412                         .collect(Collectors.toSet());
413                 int count = (ms.numClasses() > 0 ? 1 : 0) +
414                             (ms.numResources() > 0 ? 1 : 0) +
415                             (ms.numConfigs() > 0 ? 1 : 0) +
416                             (ms.numNativeLibraries() > 0 ? 1 : 0) +
417                             (ms.numNativeLibrariesDebug() > 0 ? 1 : 0) +
418                             (ms.numCommands() > 0 ? 1 : 0) +
419                             (ms.numCommandsDebug() > 0 ? 1 : 0);
420                 this.aggregator = ms.numClasses() == 1 && count == 1; // only module-info.class
421 
422                 // 5 fixed rows (name + 2 transitive count/size + 2 blank rows)
423                 this.maxRows = 5 + count + (aggregator && !aggregatorNote ? 2 : 0);
424             }
425 
writeTo(PrintStream out)426             public void writeTo(PrintStream out) {
427                 out.println(String.format("<tr id=\"%s\" class=\"%s\">", ms.name(), MODULE));
428                 out.println(moduleColumn());
429                 out.println(requiresColumn());
430                 out.println(exportsColumn());
431                 out.println(servicesColumn());
432                 out.println(otherSectionColumn());
433                 out.println("</td>");
434                 out.println("</tr>");
435             }
436 
moduleColumn()437             public String moduleColumn() {
438                 // module name
439                 StringBuilder sb = new StringBuilder("  ");
440                 sb.append("<td>");
441                 sb.append(String.format("<table class=\"%s\">", MODULE)).append("\n");
442                 sb.append(moduleName(ms.name()));
443                 sb.append(blankRow());
444                 // metadata
445                 sb.append(toTableRow("class", "classes", ms.numClasses(), ms.classBytes()));
446                 sb.append(toTableRow("resource", "resources", ms.numResources(), ms.resourceBytes()));
447                 sb.append(toTableRow("config", "configs", ms.numConfigs(), ms.configBytes()));
448                 sb.append(toTableRow("native library", "native libraries",
449                                      ms.numNativeLibraries(), ms.nativeLibrariesBytes()));
450                 sb.append(toTableRow("native library debug", "native libraries debug",
451                                      ms.numNativeLibrariesDebug(), ms.nativeLibrariesDebugBytes()));
452                 sb.append(toTableRow("command", "commands", ms.numCommands(), ms.commandBytes()));
453                 sb.append(toTableRow("command debug", "commands debug",
454                                      ms.numCommandsDebug(), ms.commandDebugBytes()));
455                 sb.append(blankRow());
456 
457                 // transitive dependencies
458                 long reqBytes = deps.stream()
459                                     .filter(d -> !d.name().equals(ms.name()))
460                                     .mapToLong(d -> modules.get(d.name()).uncompressedSize())
461                                     .sum();
462                 long reqJmodFileSize = deps.stream()
463                                             .mapToLong(d -> modules.get(d.name()).jmodFileSize())
464                                             .sum();
465                 // size
466                 if (totalBytesNote) {
467                     sb.append(toTableRow("Total bytes", ms.uncompressedSize()));
468                     sb.append(toTableRow("Total bytes of dependencies", reqBytes));
469                 } else {
470                     // print footnote
471                     sb.append(toTableRow("Total bytes<sup>1</sup>", ms.uncompressedSize()));
472                     sb.append(toTableRow("Total bytes of dependencies<sup>2</sup>", reqBytes));
473                 }
474                 String files = deps.size() == 1 ? "file" : "files";
475                 sb.append(toTableRow(String.format("Total jmod bytes (%d %s)", deps.size(), files), reqJmodFileSize));
476 
477                 if (aggregator && !aggregatorNote) {
478                     aggregatorNote = true;
479                     sb.append(blankRow());
480                     sb.append(toTableRow("<i>* aggregator is a module with module-info.class only</i>", BR));
481                 }
482                 if (!totalBytesNote) {
483                     totalBytesNote = true;
484                     sb.append(blankRow());
485                     sb.append(toTableRow("<i><sup>1</sup>sum of all files including debug files</i>", BR));
486                     sb.append(toTableRow("<i><sup>2</sup>sum of direct and indirect dependencies</i>", BR));
487                 }
488                 sb.append("</table>").append("</td>");
489                 return sb.toString();
490             }
491 
moduleName(String mn)492             private String moduleName(String mn) {
493                 if (aggregator) {
494                     StringBuilder sb = new StringBuilder();
495                     sb.append(String.format("<tr><td colspan=\"2\"><span class=\"%s\">", AGGREGATOR))
496                       .append(mn)
497                       .append("</span>").append("&nbsp;&nbsp;");
498                     if (!aggregatorNote) {
499                         sb.append("(aggregator<sup>*</sup>)");
500                     } else {
501                         sb.append("(aggregator)");
502                     }
503                     sb.append("</td></tr>");
504                     return sb.toString();
505                 } else {
506                     return toTableRow(mn, MODULE_DEF);
507                 }
508             }
509 
requiresColumn()510             public String requiresColumn() {
511                 StringBuilder sb = new StringBuilder();
512                 sb.append(String.format("<td>"));
513                 boolean footnote = requiresTransitiveNote;
514                 ms.descriptor().requires().stream()
515                         .sorted(Comparator.comparing(Requires::name))
516                         .forEach(r -> {
517                             boolean requiresTransitive = r.modifiers().contains(Requires.Modifier.TRANSITIVE);
518                             Selector sel = requiresTransitive ? REQUIRES_PUBLIC : REQUIRES;
519                             String req = String.format("<a class=\"%s\" href=\"#%s\">%s</a>",
520                                                        sel, r.name(), r.name());
521                             if (!requiresTransitiveNote && requiresTransitive) {
522                                 requiresTransitiveNote = true;
523                                 req += "<sup>*</sup>";
524                             }
525                             sb.append(req).append("\n").append("<br>");
526                         });
527 
528                 if (!ms.name().equals("java.base")) {
529                     int directDeps = ms.descriptor().requires().size();
530                     int indirectDeps = deps.size()-directDeps-1;
531                     for (int i=directDeps; i< (maxRows-1); i++) {
532                         sb.append("<br>");
533                     }
534                     sb.append("<br>");
535                     sb.append("<i>+").append(indirectDeps).append(" transitive dependencies</i>");
536                 }
537                 if (footnote != requiresTransitiveNote) {
538                     sb.append("<br><br>").append("<i>* bold denotes requires transitive</i>");
539                 }
540                 sb.append("</td>");
541                 return sb.toString();
542             }
543 
exportsColumn()544             public String exportsColumn() {
545                 StringBuilder sb = new StringBuilder();
546                 sb.append(String.format("  <td class=\"%s\">", CODE));
547                 ms.descriptor().exports().stream()
548                         .sorted(Comparator.comparing(Exports::source))
549                         .filter(e -> !e.isQualified())
550                         .forEach(e -> sb.append(e.source()).append("<br>").append("\n"));
551                 sb.append("</td>");
552                 return sb.toString();
553             }
554 
servicesColumn()555             public String servicesColumn() {
556                 StringBuilder sb = new StringBuilder();
557                 sb.append(String.format("  <td class=\"%s\">", CODE));
558                 ms.descriptor().uses().stream()
559                         .sorted()
560                         .forEach(s -> sb.append("uses ").append(s).append("<br>").append("\n"));
561                 ms.descriptor().provides().stream()
562                         .sorted(Comparator.comparing(Provides::service))
563                         .map(p -> String.format("provides %s<br>&nbsp;&nbsp;&nbsp;&nbsp;with %s",
564                                                 p.service(), p.providers()))
565                         .forEach(p -> sb.append(p).append("<br>").append("\n"));
566                 sb.append("</td>");
567                 return sb.toString();
568             }
569 
otherSectionColumn()570             public String otherSectionColumn() {
571                 StringBuilder sb = new StringBuilder();
572                 sb.append("<td>");
573                 sb.append(String.format("<table class=\"%s\">", MODULE)).append("\n");
574                 // commands
575                 if (ms.numCommands() > 0) {
576                     sb.append(toTableRow("bin/", CODE));
577                     ms.commands().entrySet().stream()
578                             .sorted(Map.Entry.comparingByKey())
579                             .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE)));
580                     sb.append(blankRow());
581                 }
582 
583                 // native libraries
584                 if (ms.numNativeLibraries() > 0) {
585                     sb.append(toTableRow("lib/", CODE));
586                     ms.nativeLibs().entrySet().stream()
587                             .sorted(Map.Entry.comparingByKey())
588                             .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE)));
589                     sb.append(blankRow());
590                 }
591 
592                 // config files
593                 if (ms.numConfigs() > 0) {
594                     sb.append(toTableRow("conf/", CODE));
595                     ms.configFiles().entrySet().stream()
596                             .sorted(Map.Entry.comparingByKey())
597                             .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE)));
598                 }
599                 // totals
600                 sb.append("</table>").append("</td>");
601                 return sb.toString();
602             }
603 
blankRow()604             private String blankRow() {
605                 return toTableRow("&nbsp;", BR);
606             }
607 
toTableRow(String col, Selector selector)608             private String toTableRow(String col, Selector selector) {
609                 TableDataBuilder builder = new TableDataBuilder();
610                 builder.colspan(selector, 2, col);
611                 return builder.build();
612             }
613 
toTableRow(String col1, long col2)614             private String toTableRow(String col1, long col2) {
615                 return toTableRow(col1, col2, BR);
616             }
617 
toTableRow(String col1, long col2, Selector selector)618             private String toTableRow(String col1, long col2, Selector selector) {
619                 TableDataBuilder builder = new TableDataBuilder();
620                 builder.data(selector, col1);
621                 builder.data(col2);
622                 return builder.build();
623 
624             }
625 
toTableRow(String singular, String plural, int count, long bytes)626             private String toTableRow(String singular, String plural, int count, long bytes) {
627                 if (count == 0) {
628                     return "";
629                 }
630                 TableDataBuilder builder = new TableDataBuilder();
631                 if (count == 1) {
632                     builder.data(count + " " + singular);
633                 } else {
634                     builder.data(count + " " + plural);
635                 }
636                 builder.data(bytes);
637                 return builder.build();
638             }
639 
640             class TableDataBuilder {
641                 private final StringBuilder sb;
TableDataBuilder()642                 TableDataBuilder() {
643                     this.sb = new StringBuilder("<tr>");
644                 }
data(String s)645                 TableDataBuilder data(String s) {
646                     data(BR, s);
647                     return this;
648                 }
data(long num)649                 TableDataBuilder data(long num) {
650                     data(NUMBER, String.format("%,d", num));
651                     return this;
652                 }
colspan(Selector selector, int columns, String data)653                 TableDataBuilder colspan(Selector selector, int columns, String data) {
654                     sb.append("<td colspan=\"").append(columns).append("\">");
655                     sb.append("<span class=\"").append(selector).append("\">");
656                     sb.append(data).append("</span></td>");
657                     return this;
658                 }
659 
data(Selector selector, String data)660                 TableDataBuilder data(Selector selector, String data) {
661                     sb.append("<td class=\"").append(selector).append("\">");
662                     sb.append(data).append("</td>");
663                     return this;
664                 }
build()665                 String build() {
666                     sb.append("</tr>");
667                     return sb.toString();
668                 }
669             }
670         }
671 
672         private static final String[] STYLES = new String[]{
673                 "<link rel=\"stylesheet\" type=\"text/css\" href=\"/.fonts/dejavu.css\"/>",
674                 "<style type=\"text/css\">",
675                 "        HTML, BODY, DIV, SPAN, APPLET, OBJECT, IFRAME, H1, H2, H3, H4, H5, H6, P,",
676                 "        BLOCKQUOTE, PRE, A, ABBR, ACRONYM, ADDRESS, BIG, CITE, CODE, DEL, DFN, EM,",
677                 "        IMG, INS, KBD, Q, S, SAMP, SMALL, STRIKE, STRONG, SUB, SUP, TT, VAR, B, U,",
678                 "        I, CENTER, DL, DT, DD, OL, UL, LI, FIELDSET, FORM, LABEL, LEGEND, TABLE,",
679                 "        CAPTION, TBODY, TFOOT, THEAD, TR, TH, TD, ARTICLE, ASIDE, CANVAS, DETAILS,",
680                 "        EMBED, FIGURE, FIGCAPTION, FOOTER, HEADER, HGROUP, MENU, NAV, OUTPUT, RUBY,",
681                 "        SECTION, SUMMARY, TIME, MARK, AUDIO, VIDEO {",
682                 "          margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit;",
683                 "          vertical-align: baseline; }",
684                 "        ARTICLE, ASIDE, DETAILS, FIGCAPTION, FIGURE, ",
685                 "        FOOTER, HEADER, HGROUP, MENU, NAV, SECTION { display: block; }",
686                 "        BLOCKQUOTE, Q { quotes: none; }",
687                 "        BLOCKQUOTE:before, BLOCKQUOTE:after, Q:before, Q:after {",
688                 "                content: ''; content: none; }",
689                 "        TABLE { border-collapse: collapse; border-spacing: 0; }",
690                 "        A { text-decoration: none; }",
691                 "        A:link { color: #437291; }",
692                 "        A:visited { color: #666666; }",
693                 "        A.anchor:link, A.anchor:visited { color: black; }",
694                 "        A[href]:hover { color: #e76f00; }",
695                 "        A IMG { border-width: 0px; }",
696                 "        HTML { font-size: 20px; } /* baseline grid */",
697                 "        HTML > BODY { font-size: 14px; }",
698                 "        BODY {",
699                 "          background: white;",
700                 "          margin: 40px;",
701                 "          margin-bottom: 150%;",
702                 "          line-height: 20px;",
703                 "          -webkit-text-size-adjust: 100%; /* iOS */",
704                 "          color: #222;",
705                 "        }",
706                 "        BODY { font-family: \"DejaVu Serif\", \"Lucida Bright\", \"Bookman Old Style\",",
707                 "                            Georgia, serif; }",
708                 "        CODE, TT, .jref, DIV.spec .open, TABLE.profiles {",
709                 "          font-family: \"DejaVu Sans\", \"Lucida Sans\", Helvetica, sans-serif; }",
710                 "        PRE, .code { font-family: \"DejaVu Sans Mono\", \"Bitstream Vera Sans Mono\",",
711                 "                            Monaco, \"Courier New\", monospace; }",
712                 "        H1, H2, H3, H4 { color: green; font-weight: bold; }",
713                 "        I { font-style: italic; }",
714                 "        TH { font-weight: bold; }",
715                 "        P { text-indent: 40px; }",
716                 "        P:first-child, UL + P, OL + P, BLOCKQUOTE + P, TABLE + P, P.subsection,",
717                 "          P.break, DIV.profiles-table + P { text-indent: 0; }",
718                 "        P.break { margin-top: 10px; }",
719                 "        P.subsection { margin-top: 20px; }",
720                 "        P.subsection SPAN.title { font-weight: bold; padding-right: 20px; }",
721                 "        UL, OL { margin: 10px 0; padding-left: 40px; }",
722                 "        LI { margin-bottom: 10px; }",
723                 "        UL.compact LI { margin-bottom: 0; }",
724                 "        PRE { padding: 0; margin: 10px 0 10px 20px; background: #eee; width: 45em; }",
725                 "        BLOCKQUOTE { margin: 10px 0; margin-left: 20px; }",
726                 "        LI BLOCKQUOTE { margin-left: 0; }",
727                 "        UL LI { list-style-type: square; }",
728                 "        .todo { color: darkred; text-align: right; }",
729                 "        .error { color: red; font-weight: bold; }",
730                 "        .warn { color: #ee0000; font-weight: bold; }",
731                 "        DIV.doctitle { margin-top: -13px;",
732                 "          font-size: 22px; line-height: 40px; font-weight: bold; }",
733                 "        DIV.twarn { color: #cc0000; font-weight: bold; margin-bottom: 9px; }",
734                 "        DIV.subtitle { margin-top: 2px; font-size: 18px; font-weight: bold; }",
735                 "        DIV.authors { margin-top: 10px; margin-bottom: 10px; font-size: 16px; }",
736                 "        DIV.author A { font-style: italic; }",
737                 "        DIV.version { margin-top: 10px; font-size: 12px; }",
738                 "        DIV.version, DIV.legal-notice { font-size: 12px; line-height: 15px; }",
739                 "        SPAN.hash { font-size: 9px; }",
740                 "        DIV.version SPAN.modified { color: green; font-weight: bold; }",
741                 "        DIV.head { margin-bottom: 20px; }",
742                 "        DIV.section > DIV.title, DIV.section DIV.number SPAN {",
743                 "          font-size: 15px; font-weight: bold; }",
744                 "        TABLE { border-collapse: collapse; border: none; }",
745                 "        TD.number { text-align: right; }",
746                 "        TD, TH { text-align: left; white-space: nowrap; }",
747                 "        TD.name, SPAN.name { font-weight: bold; }",
748                 "        ",
749                 "        TABLE.module { width: 100%; }",
750                 "        TABLE.module TD:first-child { padding-right: 10px; }",
751                 "        TR.module > TD { padding: 10px 0; border-top: 1px solid black; }",
752                 "        TR > TH { padding-bottom: 10px; }",
753                 "        TR.br TD { padding-top: 20px; }",
754                 "        TABLE.modules { margin-top: 20px; }",
755                 "        TABLE.modules > TBODY > TR > TD:nth-child(even) { background: #eee; }",
756                 "        TABLE.modules > TBODY > TR > TD, TABLE.modules > TBODY > TR > TH {",
757                 "          padding-left: 10px; padding-right: 10px; }",
758                 "        .reexp, .def { font-weight: bold; }",
759                 "        .agg { font-style: italic; }",
760                 "        SUP { height: 0; line-height: 1; position: relative;",
761                 "              vertical-align: baseline; bottom: 1ex; font-size: 11px; }",
762                 "</style>",
763         };
764     }
765 }
766