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 }