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