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