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