1 /*
   2  * Copyright (c) 2015, 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 package build.tools.module;
  26 
  27 import java.io.BufferedWriter;
  28 import java.io.IOException;
  29 import java.io.PrintWriter;
  30 import java.nio.file.Files;
  31 import java.nio.file.Path;
  32 import java.nio.file.Paths;
  33 import java.util.ArrayList;
  34 import java.util.Arrays;
  35 import java.util.Collections;
  36 import java.util.HashMap;
  37 import java.util.LinkedHashSet;
  38 import java.util.List;
  39 import java.util.Map;
  40 import java.util.Set;
  41 import java.util.stream.Stream;
  42 import static java.util.stream.Collectors.*;
  43 
  44 /**
  45  * A build tool to extend the module-info.java in the source tree for
  46  * platform-specific exports, opens, uses, and provides and write to
  47  * the specified output file.
  48  *
  49  * GenModuleInfoSource will be invoked for each module that has
  50  * module-info.java.extra in the source directory.
  51  *
  52  * The extra exports, opens, uses, provides can be specified
  53  * in module-info.java.extra.
  54  * Injecting platform-specific requires is not supported.
  55  *
  56  * @see build.tools.module.ModuleInfoExtraTest for basic testing
  57  */
  58 public class GenModuleInfoSource {
  59     private final static String USAGE =
  60             "Usage: GenModuleInfoSource \n" +
  61                     " [-d]\n" +
  62                     " -o <output file>\n" +
  63                     "  --source-file <module-info-java>\n" +
  64                     "  --modules <module-name>[,<module-name>...]\n" +
  65                     "  <module-info.java.extra> ...\n";
  66 
  67     static boolean debug = false;
  68     static boolean verbose = false;
  69     public static void main(String... args) throws Exception {
  70         Path outfile = null;
  71         Path moduleInfoJava = null;
  72         Set<String> modules = Collections.emptySet();
  73         List<Path> extras = new ArrayList<>();
  74         // validate input arguments
  75         for (int i = 0; i < args.length; i++){
  76             String option = args[i];
  77             String arg = i+1 < args.length ? args[i+1] : null;
  78             switch (option) {
  79                 case "-d":
  80                     debug = true;
  81                     break;
  82                 case "-o":
  83                     outfile = Paths.get(arg);
  84                     i++;
  85                     break;
  86                 case "--source-file":
  87                     moduleInfoJava = Paths.get(arg);
  88                     if (Files.notExists(moduleInfoJava)) {
  89                         throw new IllegalArgumentException(moduleInfoJava + " not exist");
  90                     }
  91                     i++;
  92                     break;
  93                 case "--modules":
  94                     modules = Arrays.stream(arg.split(","))
  95                                     .collect(toSet());
  96                     i++;
  97                     break;
  98                 case "-v":
  99                     verbose = true;
 100                     break;
 101                 default:
 102                     Path file = Paths.get(option);
 103                     if (Files.notExists(file)) {
 104                         throw new IllegalArgumentException(file + " not exist");
 105                     }
 106                     extras.add(file);
 107             }
 108         }
 109 
 110         if (moduleInfoJava == null || outfile == null ||
 111                 modules.isEmpty() || extras.isEmpty()) {
 112             System.err.println(USAGE);
 113             System.exit(-1);
 114         }
 115 
 116         GenModuleInfoSource genModuleInfo =
 117             new GenModuleInfoSource(moduleInfoJava, extras, modules);
 118 
 119         // generate new module-info.java
 120         genModuleInfo.generate(outfile);
 121     }
 122 
 123     final Path sourceFile;
 124     final List<Path> extraFiles;
 125     final ModuleInfo extras;
 126     final Set<String> modules;
 127     final ModuleInfo moduleInfo;
 128     GenModuleInfoSource(Path sourceFile, List<Path> extraFiles, Set<String> modules)
 129         throws IOException
 130     {
 131         this.sourceFile = sourceFile;
 132         this.extraFiles = extraFiles;
 133         this.modules = modules;
 134         this.moduleInfo = new ModuleInfo();
 135         this.moduleInfo.parse(sourceFile);
 136 
 137         // parse module-info.java.extra
 138         this.extras = new ModuleInfo();
 139         for (Path file : extraFiles) {
 140             extras.parse(file);
 141         }
 142 
 143         // merge with module-info.java.extra
 144         moduleInfo.augmentModuleInfo(extras, modules);
 145     }
 146 
 147     void generate(Path output) throws IOException {
 148         List<String> lines = Files.readAllLines(sourceFile);
 149         try (BufferedWriter bw = Files.newBufferedWriter(output);
 150              PrintWriter writer = new PrintWriter(bw)) {
 151             // write the copyright header and lines up to module declaration
 152             for (String l : lines) {
 153                 writer.println(l);
 154                 if (l.trim().startsWith("module ")) {
 155                     if (debug) {
 156                         // print URI rather than file path to avoid escape
 157                         writer.format("    // source file: %s%n", sourceFile.toUri());
 158                         for (Path file : extraFiles) {
 159                             writer.format("    //              %s%n", file.toUri());
 160                         }
 161                     }
 162                     break;
 163                 }
 164             }
 165 
 166             // requires
 167             for (String l : lines) {
 168                 if (l.trim().startsWith("requires"))
 169                     writer.println(l);
 170             }
 171 
 172             // write exports, opens, uses, and provides
 173             moduleInfo.print(writer);
 174 
 175             // close
 176             writer.println("}");
 177         }
 178     }
 179 
 180 
 181     class ModuleInfo {
 182         final Map<String, Statement> exports = new HashMap<>();
 183         final Map<String, Statement> opens = new HashMap<>();
 184         final Map<String, Statement> uses = new HashMap<>();
 185         final Map<String, Statement> provides = new HashMap<>();
 186 
 187         Statement getStatement(String directive, String name) {
 188             switch (directive) {
 189                 case "exports":
 190                     if (moduleInfo.exports.containsKey(name) &&
 191                         moduleInfo.exports.get(name).isUnqualified()) {
 192                         throw new IllegalArgumentException(sourceFile +
 193                             " already has " + directive + " " + name);
 194                     }
 195                     return exports.computeIfAbsent(name,
 196                         _n -> new Statement("exports", "to", name));
 197 
 198                 case "opens":
 199                     if (moduleInfo.opens.containsKey(name) &&
 200                         moduleInfo.opens.get(name).isUnqualified()) {
 201                         throw new IllegalArgumentException(sourceFile +
 202                             " already has " + directive + " " + name);
 203                     }
 204 
 205                     if (moduleInfo.opens.containsKey(name)) {
 206                         throw new IllegalArgumentException(sourceFile +
 207                             " already has " + directive + " " + name);
 208                     }
 209                     return opens.computeIfAbsent(name,
 210                         _n -> new Statement("opens", "to", name));
 211 
 212                 case "uses":
 213                     return uses.computeIfAbsent(name,
 214                         _n -> new Statement("uses", "", name));
 215 
 216                 case "provides":
 217                     return provides.computeIfAbsent(name,
 218                         _n -> new Statement("provides", "with", name, true));
 219 
 220                 default:
 221                     throw new IllegalArgumentException(directive);
 222             }
 223 
 224         }
 225 
 226         /*
 227          * Augment this ModuleInfo with module-info.java.extra
 228          */
 229         void augmentModuleInfo(ModuleInfo extraFiles, Set<String> modules) {
 230             // API package exported in the original module-info.java
 231             extraFiles.exports.entrySet()
 232                 .stream()
 233                 .filter(e -> exports.containsKey(e.getKey()) &&
 234                                 e.getValue().filter(modules))
 235                 .forEach(e -> mergeExportsOrOpens(exports.get(e.getKey()),
 236                                                   e.getValue(),
 237                                                   modules));
 238 
 239             // add exports that are not defined in the original module-info.java
 240             extraFiles.exports.entrySet()
 241                 .stream()
 242                 .filter(e -> !exports.containsKey(e.getKey()) &&
 243                                 e.getValue().filter(modules))
 244                 .forEach(e -> addTargets(getStatement("exports", e.getKey()),
 245                                          e.getValue(),
 246                                          modules));
 247 
 248             // API package opened in the original module-info.java
 249             extraFiles.opens.entrySet()
 250                 .stream()
 251                 .filter(e -> opens.containsKey(e.getKey()) &&
 252                                 e.getValue().filter(modules))
 253                 .forEach(e -> mergeExportsOrOpens(opens.get(e.getKey()),
 254                                                   e.getValue(),
 255                                                   modules));
 256 
 257             // add opens that are not defined in the original module-info.java
 258             extraFiles.opens.entrySet()
 259                 .stream()
 260                 .filter(e -> !opens.containsKey(e.getKey()) &&
 261                                 e.getValue().filter(modules))
 262                 .forEach(e -> addTargets(getStatement("opens", e.getKey()),
 263                                          e.getValue(),
 264                                          modules));
 265 
 266             // provides
 267             extraFiles.provides.keySet()
 268                 .stream()
 269                 .filter(service -> provides.containsKey(service))
 270                 .forEach(service -> mergeProvides(service,
 271                                                   extraFiles.provides.get(service)));
 272             extraFiles.provides.keySet()
 273                 .stream()
 274                 .filter(service -> !provides.containsKey(service))
 275                 .forEach(service -> provides.put(service,
 276                                                  extraFiles.provides.get(service)));
 277 
 278             // uses
 279             extraFiles.uses.keySet()
 280                 .stream()
 281                 .filter(service -> !uses.containsKey(service))
 282                 .forEach(service -> uses.put(service, extraFiles.uses.get(service)));
 283         }
 284 
 285         // add qualified exports or opens to known modules only
 286         private void addTargets(Statement statement,
 287                                 Statement extra,
 288                                 Set<String> modules)
 289         {
 290             extra.targets.stream()
 291                  .filter(mn -> modules.contains(mn))
 292                  .forEach(mn -> statement.addTarget(mn));
 293         }
 294 
 295         private void mergeExportsOrOpens(Statement statement,
 296                                          Statement extra,
 297                                          Set<String> modules)
 298         {
 299             String pn = statement.name;
 300             if (statement.isUnqualified() && extra.isQualified()) {
 301                 throw new RuntimeException("can't add qualified exports to " +
 302                     "unqualified exports " + pn);
 303             }
 304 
 305             Set<String> mods = extra.targets.stream()
 306                 .filter(mn -> statement.targets.contains(mn))
 307                 .collect(toSet());
 308             if (mods.size() > 0) {
 309                 throw new RuntimeException("qualified exports " + pn + " to " +
 310                     mods.toString() + " already declared in " + sourceFile);
 311             }
 312 
 313             // add qualified exports or opens to known modules only
 314             addTargets(statement, extra, modules);
 315         }
 316 
 317         private void mergeProvides(String service, Statement extra) {
 318             Statement statement = provides.get(service);
 319 
 320             Set<String> mods = extra.targets.stream()
 321                 .filter(mn -> statement.targets.contains(mn))
 322                 .collect(toSet());
 323 
 324             if (mods.size() > 0) {
 325                 throw new RuntimeException("qualified exports " + service + " to " +
 326                     mods.toString() + " already declared in " + sourceFile);
 327             }
 328 
 329             extra.targets.stream()
 330                  .forEach(mn -> statement.addTarget(mn));
 331         }
 332 
 333 
 334         void print(PrintWriter writer) {
 335             // print unqualified exports
 336             exports.entrySet().stream()
 337                 .filter(e -> e.getValue().targets.isEmpty())
 338                 .sorted(Map.Entry.comparingByKey())
 339                 .forEach(e -> writer.println(e.getValue()));
 340 
 341             // print qualified exports
 342             exports.entrySet().stream()
 343                 .filter(e -> !e.getValue().targets.isEmpty())
 344                 .sorted(Map.Entry.comparingByKey())
 345                 .forEach(e -> writer.println(e.getValue()));
 346 
 347             // print unqualified opens
 348             opens.entrySet().stream()
 349                 .filter(e -> e.getValue().targets.isEmpty())
 350                 .sorted(Map.Entry.comparingByKey())
 351                 .forEach(e -> writer.println(e.getValue()));
 352 
 353             // print qualified opens
 354             opens.entrySet().stream()
 355                 .filter(e -> !e.getValue().targets.isEmpty())
 356                 .sorted(Map.Entry.comparingByKey())
 357                 .forEach(e -> writer.println(e.getValue()));
 358 
 359             // uses and provides
 360             writer.println();
 361             uses.entrySet().stream()
 362                 .sorted(Map.Entry.comparingByKey())
 363                 .forEach(e -> writer.println(e.getValue()));
 364             provides.entrySet().stream()
 365                 .sorted(Map.Entry.comparingByKey())
 366                 .forEach(e -> writer.println(e.getValue()));
 367         }
 368 
 369         private void parse(Path sourcefile) throws IOException {
 370             List<String> lines = Files.readAllLines(sourcefile);
 371             Statement statement = null;
 372             boolean hasTargets = false;
 373 
 374             for (int lineNumber = 1; lineNumber <= lines.size(); ) {
 375                 String l = lines.get(lineNumber-1).trim();
 376                 int index = 0;
 377 
 378                 if (l.isEmpty()) {
 379                     lineNumber++;
 380                     continue;
 381                 }
 382 
 383                 // comment block starts
 384                 if (l.startsWith("/*")) {
 385                     while (l.indexOf("*/") == -1) { // end comment block
 386                         l = lines.get(lineNumber++).trim();
 387                     }
 388                     index = l.indexOf("*/") + 2;
 389                     if (index >= l.length()) {
 390                         lineNumber++;
 391                         continue;
 392                     } else {
 393                         // rest of the line
 394                         l = l.substring(index, l.length()).trim();
 395                         index = 0;
 396                     }
 397                 }
 398 
 399                 // skip comment and annotations
 400                 if (l.startsWith("//") || l.startsWith("@")) {
 401                     lineNumber++;
 402                     continue;
 403                 }
 404 
 405                 int current = lineNumber;
 406                 int count = 0;
 407                 while (index < l.length()) {
 408                     if (current == lineNumber && ++count > 20)
 409                         throw new Error("Fail to parse line " + lineNumber + " " + sourcefile);
 410 
 411                     int end = l.indexOf(';');
 412                     if (end == -1)
 413                         end = l.length();
 414                     String content = l.substring(0, end).trim();
 415                     if (content.isEmpty()) {
 416                         index = end+1;
 417                         if (index < l.length()) {
 418                             // rest of the line
 419                             l = l.substring(index, l.length()).trim();
 420                             index = 0;
 421                         }
 422                         continue;
 423                     }
 424 
 425                     String[] s = content.split("\\s+");
 426                     String keyword = s[0].trim();
 427 
 428                     String name = s.length > 1 ? s[1].trim() : null;
 429                     trace("%d: %s index=%d len=%d%n", lineNumber, l, index, l.length());
 430                     switch (keyword) {
 431                         case "module":
 432                         case "requires":
 433                         case "}":
 434                             index = l.length();  // skip to the end
 435                             continue;
 436 
 437                         case "exports":
 438                         case "opens":
 439                         case "provides":
 440                         case "uses":
 441                             // assume name immediately after exports, opens, provides, uses
 442                             statement = getStatement(keyword, name);
 443                             hasTargets = false;
 444 
 445                             int i = l.indexOf(name, keyword.length()+1) + name.length() + 1;
 446                             l = i < l.length() ? l.substring(i, l.length()).trim() : "";
 447                             index = 0;
 448 
 449                             if (s.length >= 3) {
 450                                 if (!s[2].trim().equals(statement.qualifier)) {
 451                                     throw new RuntimeException(sourcefile + ", line " +
 452                                         lineNumber + ", is malformed: " + s[2]);
 453                                 }
 454                             }
 455 
 456                             break;
 457 
 458                         case "to":
 459                         case "with":
 460                             if (statement == null) {
 461                                 throw new RuntimeException(sourcefile + ", line " +
 462                                     lineNumber + ", is malformed");
 463                             }
 464 
 465                             hasTargets = true;
 466                             String qualifier = statement.qualifier;
 467                             i = l.indexOf(qualifier, index) + qualifier.length() + 1;
 468                             l = i < l.length() ? l.substring(i, l.length()).trim() : "";
 469                             index = 0;
 470                             break;
 471                     }
 472 
 473                     if (index >= l.length()) {
 474                         // skip to next line
 475                         continue;
 476                     }
 477 
 478                         // comment block starts
 479                     if (l.startsWith("/*")) {
 480                         while (l.indexOf("*/") == -1) { // end comment block
 481                             l = lines.get(lineNumber++).trim();
 482                         }
 483                         index = l.indexOf("*/") + 2;
 484                         if (index >= l.length()) {
 485                             continue;
 486                         } else {
 487                             // rest of the line
 488                             l = l.substring(index, l.length()).trim();
 489                             index = 0;
 490                         }
 491                     }
 492 
 493                     if (l.startsWith("//")) {
 494                         index = l.length();
 495                         continue;
 496                     }
 497 
 498                     if (statement == null) {
 499                         throw new RuntimeException(sourcefile + ", line " +
 500                             lineNumber + ": missing keyword?");
 501                     }
 502 
 503                     if (!hasTargets) {
 504                         continue;
 505                     }
 506 
 507                     if (index >= l.length()) {
 508                         throw new RuntimeException(sourcefile + ", line " +
 509                             lineNumber + ": " + l);
 510                     }
 511 
 512                     // parse the target module of exports, opens, or provides
 513                     Statement stmt = statement;
 514 
 515                     int terminal = l.indexOf(';', index);
 516                     // determine up to which position to parse
 517                     int pos = terminal != -1 ? terminal : l.length();
 518                     // parse up to comments
 519                     int pos1 = l.indexOf("//", index);
 520                     if (pos1 != -1 && pos1 < pos) {
 521                         pos = pos1;
 522                     }
 523                     int pos2 = l.indexOf("/*", index);
 524                     if (pos2 != -1 && pos2 < pos) {
 525                         pos = pos2;
 526                     }
 527                     // target module(s) for qualitifed exports or opens
 528                     // or provider implementation class(es)
 529                     String rhs = l.substring(index, pos).trim();
 530                     index += rhs.length();
 531                     trace("rhs: index=%d [%s] [line: %s]%n", index, rhs, l);
 532 
 533                     String[] targets = rhs.split(",");
 534                     for (String t : targets) {
 535                         String n = t.trim();
 536                         if (n.length() > 0)
 537                             stmt.addTarget(n);
 538                     }
 539 
 540                     // start next statement
 541                     if (pos == terminal) {
 542                         statement = null;
 543                         hasTargets = false;
 544                         index = terminal + 1;
 545                     }
 546                     l = index < l.length() ? l.substring(index, l.length()).trim() : "";
 547                     index = 0;
 548                 }
 549 
 550                 lineNumber++;
 551             }
 552         }
 553     }
 554 
 555     static class Statement {
 556         final String directive;
 557         final String qualifier;
 558         final String name;
 559         final Set<String> targets = new LinkedHashSet<>();
 560         final boolean ordered;
 561 
 562         Statement(String directive, String qualifier, String name) {
 563             this(directive, qualifier, name, false);
 564         }
 565 
 566         Statement(String directive, String qualifier, String name, boolean ordered) {
 567             this.directive = directive;
 568             this.qualifier = qualifier;
 569             this.name = name;
 570             this.ordered = ordered;
 571         }
 572 
 573         Statement addTarget(String mn) {
 574             if (mn.isEmpty())
 575                 throw new IllegalArgumentException("empty module name");
 576             targets.add(mn);
 577             return this;
 578         }
 579 
 580         boolean isQualified() {
 581             return targets.size() > 0;
 582         }
 583 
 584         boolean isUnqualified() {
 585             return targets.isEmpty();
 586         }
 587 
 588         /**
 589          * Returns true if this statement is unqualified or it has
 590          * at least one target in the given names.
 591          */
 592         boolean filter(Set<String> names) {
 593             if (isUnqualified()) {
 594                 return true;
 595             } else {
 596                 return targets.stream()
 597                     .filter(mn -> names.contains(mn))
 598                     .findAny().isPresent();
 599             }
 600         }
 601 
 602         @Override
 603         public String toString() {
 604             StringBuilder sb = new StringBuilder("    ");
 605             sb.append(directive).append(" ").append(name);
 606             if (targets.isEmpty()) {
 607                 sb.append(";");
 608             } else if (targets.size() == 1) {
 609                 sb.append(" ").append(qualifier)
 610                   .append(orderedTargets().collect(joining(",", " ", ";")));
 611             } else {
 612                 sb.append(" ").append(qualifier)
 613                   .append(orderedTargets()
 614                       .map(target -> String.format("        %s", target))
 615                       .collect(joining(",\n", "\n", ";")));
 616             }
 617             return sb.toString();
 618         }
 619 
 620         public Stream<String> orderedTargets() {
 621             return ordered ? targets.stream()
 622                            : targets.stream().sorted();
 623         }
 624     }
 625 
 626     static void trace(String fmt, Object... params) {
 627         if (verbose) {
 628             System.out.format(fmt, params);
 629         }
 630     }
 631 }