make/jdk/src/classes/build/tools/module/GenModuleInfoSource.java
author herrick
Thu, 28 Mar 2019 13:49:38 -0400
branchJDK-8200758-branch
changeset 57292 7a683c461b80
parent 52804 28094715ae71
permissions -rw-r--r--
Merge

/*
 * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package build.tools.module;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.*;

/**
 * A build tool to extend the module-info.java in the source tree for
 * platform-specific exports, opens, uses, and provides and write to
 * the specified output file.
 *
 * GenModuleInfoSource will be invoked for each module that has
 * module-info.java.extra in the source directory.
 *
 * The extra exports, opens, uses, provides can be specified
 * in module-info.java.extra.
 * Injecting platform-specific requires is not supported.
 *
 * @see build.tools.module.ModuleInfoExtraTest for basic testing
 */
public class GenModuleInfoSource {
    private final static String USAGE =
        "Usage: GenModuleInfoSource -o <output file> \n" +
        "  --source-file <module-info-java>\n" +
        "  --modules <module-name>[,<module-name>...]\n" +
        "  <module-info.java.extra> ...\n";

    static boolean verbose = false;
    public static void main(String... args) throws Exception {
        Path outfile = null;
        Path moduleInfoJava = null;
        Set<String> modules = Collections.emptySet();
        List<Path> extras = new ArrayList<>();
        // validate input arguments
        for (int i = 0; i < args.length; i++){
            String option = args[i];
            String arg = i+1 < args.length ? args[i+1] : null;
            switch (option) {
                case "-o":
                    outfile = Paths.get(arg);
                    i++;
                    break;
                case "--source-file":
                    moduleInfoJava = Paths.get(arg);
                    if (Files.notExists(moduleInfoJava)) {
                        throw new IllegalArgumentException(moduleInfoJava + " not exist");
                    }
                    i++;
                    break;
                case "--modules":
                    modules = Arrays.stream(arg.split(","))
                                    .collect(toSet());
                    i++;
                    break;
                case "-v":
                    verbose = true;
                    break;
                default:
                    Path file = Paths.get(option);
                    if (Files.notExists(file)) {
                        throw new IllegalArgumentException(file + " not exist");
                    }
                    extras.add(file);
            }
        }

        if (moduleInfoJava == null || outfile == null ||
                modules.isEmpty() || extras.isEmpty()) {
            System.err.println(USAGE);
            System.exit(-1);
        }

        GenModuleInfoSource genModuleInfo =
            new GenModuleInfoSource(moduleInfoJava, extras, modules);

        // generate new module-info.java
        genModuleInfo.generate(outfile);
    }

    final Path sourceFile;
    final List<Path> extraFiles;
    final ModuleInfo extras;
    final Set<String> modules;
    final ModuleInfo moduleInfo;
    GenModuleInfoSource(Path sourceFile, List<Path> extraFiles, Set<String> modules)
        throws IOException
    {
        this.sourceFile = sourceFile;
        this.extraFiles = extraFiles;
        this.modules = modules;
        this.moduleInfo = new ModuleInfo();
        this.moduleInfo.parse(sourceFile);

        // parse module-info.java.extra
        this.extras = new ModuleInfo();
        for (Path file : extraFiles) {
            extras.parseExtra(file);
        }

        // merge with module-info.java.extra
        moduleInfo.augmentModuleInfo(extras, modules);
    }

    void generate(Path output) throws IOException {
        List<String> lines = Files.readAllLines(sourceFile);
        try (BufferedWriter bw = Files.newBufferedWriter(output);
             PrintWriter writer = new PrintWriter(bw)) {
            // write the copyright header and lines up to module declaration
            for (String l : lines) {
                writer.println(l);
                if (l.trim().startsWith("module ")) {
                    // print URI rather than file path to avoid escape
                    writer.format("    // source file: %s%n", sourceFile.toUri());
                    for (Path file: extraFiles) {
                        writer.format("    //              %s%n", file.toUri());
                    }
                    break;
                }
            }

            // requires
            for (String l : lines) {
                if (l.trim().startsWith("requires"))
                    writer.println(l);
            }

            // write exports, opens, uses, and provides
            moduleInfo.print(writer);

            // close
            writer.println("}");
        }
    }


    class ModuleInfo {
        final Map<String, Statement> exports = new HashMap<>();
        final Map<String, Statement> opens = new HashMap<>();
        final Map<String, Statement> uses = new HashMap<>();
        final Map<String, Statement> provides = new HashMap<>();

        Statement getStatement(String directive, String name) {
            Objects.requireNonNull(name);
            switch (directive) {
                case "exports":
                    if (moduleInfo.exports.containsKey(name) &&
                        moduleInfo.exports.get(name).isUnqualified()) {
                        throw new IllegalArgumentException(sourceFile +
                            " already has " + directive + " " + name);
                    }
                    return exports.computeIfAbsent(name,
                        _n -> new Statement("exports", "to", name));

                case "opens":
                    if (moduleInfo.opens.containsKey(name) &&
                        moduleInfo.opens.get(name).isUnqualified()) {
                        throw new IllegalArgumentException(sourceFile +
                            " already has " + directive + " " + name);
                    }

                    if (moduleInfo.opens.containsKey(name)) {
                        throw new IllegalArgumentException(sourceFile +
                            " already has " + directive + " " + name);
                    }
                    return opens.computeIfAbsent(name,
                        _n -> new Statement("opens", "to", name));

                case "uses":
                    return uses.computeIfAbsent(name,
                        _n -> new Statement("uses", "", name));

                case "provides":
                    return provides.computeIfAbsent(name,
                        _n -> new Statement("provides", "with", name, true));

                default:
                    throw new IllegalArgumentException(directive);
            }

        }

        /*
         * Augment this ModuleInfo with module-info.java.extra
         */
        void augmentModuleInfo(ModuleInfo extraFiles, Set<String> modules) {
            // API package exported in the original module-info.java
            extraFiles.exports.entrySet()
                .stream()
                .filter(e -> exports.containsKey(e.getKey()) &&
                    e.getValue().filter(modules))
                .forEach(e -> mergeExportsOrOpens(exports.get(e.getKey()),
                    e.getValue(),
                    modules));

            // add exports that are not defined in the original module-info.java
            extraFiles.exports.entrySet()
                .stream()
                .filter(e -> !exports.containsKey(e.getKey()) &&
                    e.getValue().filter(modules))
                .forEach(e -> addTargets(getStatement("exports", e.getKey()),
                    e.getValue(),
                    modules));

            // API package opened in the original module-info.java
            extraFiles.opens.entrySet()
                .stream()
                .filter(e -> opens.containsKey(e.getKey()) &&
                    e.getValue().filter(modules))
                .forEach(e -> mergeExportsOrOpens(opens.get(e.getKey()),
                    e.getValue(),
                    modules));

            // add opens that are not defined in the original module-info.java
            extraFiles.opens.entrySet()
                .stream()
                .filter(e -> !opens.containsKey(e.getKey()) &&
                    e.getValue().filter(modules))
                .forEach(e -> addTargets(getStatement("opens", e.getKey()),
                    e.getValue(),
                    modules));

            // provides
            extraFiles.provides.keySet()
                .stream()
                .filter(service -> provides.containsKey(service))
                .forEach(service -> mergeProvides(service,
                    extraFiles.provides.get(service)));
            extraFiles.provides.keySet()
                .stream()
                .filter(service -> !provides.containsKey(service))
                .forEach(service -> provides.put(service,
                    extraFiles.provides.get(service)));

            // uses
            extraFiles.uses.keySet()
                .stream()
                .filter(service -> !uses.containsKey(service))
                .forEach(service -> uses.put(service, extraFiles.uses.get(service)));
        }

        // add qualified exports or opens to known modules only
        private void addTargets(Statement statement,
                                Statement extra,
                                Set<String> modules)
        {
            extra.targets.stream()
                .filter(mn -> modules.contains(mn))
                .forEach(mn -> statement.addTarget(mn));
        }

        private void mergeExportsOrOpens(Statement statement,
                                         Statement extra,
                                         Set<String> modules)
        {
            String pn = statement.name;
            if (statement.isUnqualified() && extra.isQualified()) {
                throw new RuntimeException("can't add qualified exports to " +
                    "unqualified exports " + pn);
            }

            Set<String> mods = extra.targets.stream()
                .filter(mn -> statement.targets.contains(mn))
                .collect(toSet());
            if (mods.size() > 0) {
                throw new RuntimeException("qualified exports " + pn + " to " +
                    mods.toString() + " already declared in " + sourceFile);
            }

            // add qualified exports or opens to known modules only
            addTargets(statement, extra, modules);
        }

        private void mergeProvides(String service, Statement extra) {
            Statement statement = provides.get(service);

            Set<String> mods = extra.targets.stream()
                .filter(mn -> statement.targets.contains(mn))
                .collect(toSet());

            if (mods.size() > 0) {
                throw new RuntimeException("qualified exports " + service + " to " +
                    mods.toString() + " already declared in " + sourceFile);
            }

            extra.targets.stream()
                .forEach(mn -> statement.addTarget(mn));
        }


        void print(PrintWriter writer) {
            // print unqualified exports
            exports.entrySet().stream()
                .filter(e -> e.getValue().targets.isEmpty())
                .sorted(Map.Entry.comparingByKey())
                .forEach(e -> writer.println(e.getValue()));

            // print qualified exports
            exports.entrySet().stream()
                .filter(e -> !e.getValue().targets.isEmpty())
                .sorted(Map.Entry.comparingByKey())
                .forEach(e -> writer.println(e.getValue()));

            // print unqualified opens
            opens.entrySet().stream()
                .filter(e -> e.getValue().targets.isEmpty())
                .sorted(Map.Entry.comparingByKey())
                .forEach(e -> writer.println(e.getValue()));

            // print qualified opens
            opens.entrySet().stream()
                .filter(e -> !e.getValue().targets.isEmpty())
                .sorted(Map.Entry.comparingByKey())
                .forEach(e -> writer.println(e.getValue()));

            // uses and provides
            writer.println();
            uses.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(e -> writer.println(e.getValue()));
            provides.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach(e -> writer.println(e.getValue()));
        }


        private void parse(Path file) throws IOException {
            Parser parser = new Parser(file);
            parser.run();
            if (verbose) {
                parser.dump();
            }
            process(parser, false);
        }

        private void parseExtra(Path file) throws IOException {
            Parser parser = new Parser(file);
            parser.run();
            if (verbose) {
                parser.dump();
            }
            process(parser, true);
        }


        private void process(Parser parser, boolean extraFile) throws IOException {
            // no duplicate statement local in each file
            Map<String, Statement> exports = new HashMap<>();
            Map<String, Statement> opens = new HashMap<>();
            Map<String, Statement> uses = new HashMap<>();
            Map<String, Statement> provides = new HashMap<>();

            String token = null;
            boolean hasCurlyBracket = false;
            while ((token = parser.nextToken()) != null) {
                if (token.equals("module")) {
                    String modulename = nextIdentifier(parser);
                    if (extraFile) {
                        throw parser.newError("cannot declare module in " + parser.sourceFile);
                    }
                    skipTokenOrThrow(parser, "{", "missing {");
                    hasCurlyBracket = true;
                } else if (token.equals("requires")) {
                    token = nextIdentifier(parser);
                    if (token.equals("transitive")) {
                        token = nextIdentifier(parser);
                    }
                    if (extraFile) {
                        throw parser.newError("cannot declare requires in " + parser.sourceFile);
                    }
                    skipTokenOrThrow(parser, ";", "missing semicolon");
                } else if (isExportsOpensProvidesUses(token)) {
                    // new statement
                    String keyword = token;
                    String name = nextIdentifier(parser);
                    Statement statement = getStatement(keyword, name);
                    switch (keyword) {
                        case "exports":
                            if (exports.containsKey(name)) {
                                throw parser.newError("multiple " + keyword + " " + name);
                            }
                            exports.put(name, statement);
                            break;
                        case "opens":
                            if (opens.containsKey(name)) {
                                throw parser.newError("multiple " + keyword + " " + name);
                            }
                            opens.put(name, statement);
                            break;
                        case "uses":
                            if (uses.containsKey(name)) {
                                throw parser.newError("multiple " + keyword + " " + name);
                            }
                            uses.put(name, statement);
                            break;
                        case "provides":
                            if (provides.containsKey(name)) {
                                throw parser.newError("multiple " + keyword + " " + name);
                            }
                            provides.put(name, statement);
                            break;
                    }
                    String lookAhead = lookAhead(parser);
                    if (lookAhead.equals(statement.qualifier)) {
                        parser.nextToken(); // skip qualifier
                        while ((lookAhead = parser.peekToken()) != null) {
                            // add target name
                            name = nextIdentifier(parser);
                            statement.addTarget(name);
                            lookAhead = lookAhead(parser);
                            if (lookAhead.equals(",") || lookAhead.equals(";")) {
                                parser.nextToken();
                            } else {
                                throw parser.newError("missing semicolon");
                            }
                            if (lookAhead.equals(";")) {
                                break;
                            }
                        }
                    } else {
                        skipTokenOrThrow(parser, ";", "missing semicolon");
                    }
                } else if (token.equals(";")) {
                    continue;
                } else if (hasCurlyBracket && token.equals("}")) {
                    hasCurlyBracket = false;
                    if (parser.peekToken() != null) {  // must be EOF
                        throw parser.newError("is malformed");
                    }
                } else {
                    throw parser.newError("missing keyword");
                }
            }
            if (hasCurlyBracket) {
                parser.newError("missing }");
            }
        }

        private boolean isExportsOpensProvidesUses(String word) {
            switch (word) {
                case "exports":
                case "opens":
                case "provides":
                case "uses":
                    return true;
                default:
                    return false;
            }
        }

        private String lookAhead(Parser parser) {
            String lookAhead = parser.peekToken();
            if (lookAhead == null) { // EOF
                throw parser.newError("reach end of file");
            }
            return lookAhead;
        }

        private String nextIdentifier(Parser parser) {
            String lookAhead = parser.peekToken();
            boolean maybeIdentifier = true;
            switch (lookAhead) {
                case "module":
                case "requires":
                case "exports":
                case "opens":
                case "provides":
                case "uses":
                case "to":
                case "with":
                case ",":
                case ";":
                case "{":
                case "}":
                    maybeIdentifier = false;
            }
            if (lookAhead == null || !maybeIdentifier) {
                throw parser.newError("<identifier> missing");
            }

            return parser.nextToken();
        }

        private String skipTokenOrThrow(Parser parser, String token, String msg) {
            // look ahead to report the proper line number
            String lookAhead = parser.peekToken();
            if (!token.equals(lookAhead)) {
                throw parser.newError(msg);
            }
            return parser.nextToken();
        }
    }

    static class Statement {
        final String directive;
        final String qualifier;
        final String name;
        final Set<String> targets = new LinkedHashSet<>();
        final boolean ordered;

        Statement(String directive, String qualifier, String name) {
            this(directive, qualifier, name, false);
        }

        Statement(String directive, String qualifier, String name, boolean ordered) {
            this.directive = directive;
            this.qualifier = qualifier;
            this.name = name;
            this.ordered = ordered;
        }

        Statement addTarget(String mn) {
            if (mn.isEmpty())
                throw new IllegalArgumentException("empty module name");
            targets.add(mn);
            return this;
        }

        boolean isQualified() {
            return targets.size() > 0;
        }

        boolean isUnqualified() {
            return targets.isEmpty();
        }

        /**
         * Returns true if this statement is unqualified or it has
         * at least one target in the given names.
         */
        boolean filter(Set<String> names) {
            if (isUnqualified()) {
                return true;
            } else {
                return targets.stream()
                    .filter(mn -> names.contains(mn))
                    .findAny().isPresent();
            }
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("    ");
            sb.append(directive).append(" ").append(name);
            if (targets.isEmpty()) {
                sb.append(";");
            } else if (targets.size() == 1) {
                sb.append(" ").append(qualifier)
                  .append(orderedTargets().collect(joining(",", " ", ";")));
            } else {
                sb.append(" ").append(qualifier)
                  .append(orderedTargets()
                      .map(target -> String.format("        %s", target))
                      .collect(joining(",\n", "\n", ";")));
            }
            return sb.toString();
        }

        public Stream<String> orderedTargets() {
            return ordered ? targets.stream()
                           : targets.stream().sorted();
        }
    }

    static void trace(String fmt, Object... params) {
        if (verbose) {
            System.out.format(fmt, params);
        }
    }

    static class Parser {
        private static final List<String> EMPTY = List.of();

        private final Path sourceFile;
        private boolean inCommentBlock = false;
        private List<List<String>> tokens = new ArrayList<>();
        private int lineNumber = 1;
        private int index = 0;

        Parser(Path file) {
            this.sourceFile = file;
        }

        void run() throws IOException {
            List<String> lines = Files.readAllLines(sourceFile);
            for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
                String l = lines.get(lineNumber - 1).trim();
                tokenize(l);
            }
        }

        /*
         * Tokenize the given string.  Comments are skipped.
         */
        List<String> tokenize(String l) {
            while (!l.isEmpty()) {
                if (inCommentBlock) {
                    int comment = l.indexOf("*/");
                    if (comment == -1)
                        return emptyTokens();

                    // end comment block
                    inCommentBlock = false;
                    if ((comment + 2) >= l.length()) {
                        return emptyTokens();
                    }
                    l = l.substring(comment + 2, l.length()).trim();
                }

                // skip comment
                int comment = l.indexOf("//");
                if (comment >= 0) {
                    l = l.substring(0, comment).trim();
                    if (l.isEmpty()) return emptyTokens();
                }

                if (l.isEmpty()) {
                    return emptyTokens();
                }

                int beginComment = l.indexOf("/*");
                int endComment = l.indexOf("*/");
                if (beginComment == -1)
                    return tokens(l);

                String s1 = l.substring(0, beginComment).trim();
                if (endComment > 0) {
                    String s2 = l.substring(endComment + 2, l.length()).trim();
                    if (s1.isEmpty()) {
                        l = s2;
                    } else if (s2.isEmpty()) {
                        l = s1;
                    } else {
                        l = s1 + " " + s2;
                    }
                } else {
                    inCommentBlock = true;
                    return tokens(s1);
                }
            }
            return tokens(l);
        }

        private List<String> emptyTokens() {
            this.tokens.add(EMPTY);
            return EMPTY;
        }
        private List<String> tokens(String l) {
            List<String> tokens = new ArrayList<>();
            for (String s : l.split("\\s+")) {
                int pos=0;
                s = s.trim();
                if (s.isEmpty())
                     continue;

                int i = s.indexOf(',', pos);
                int j = s.indexOf(';', pos);
                while ((i >= 0 && i < s.length()) || (j >= 0 && j < s.length())) {
                    if (j == -1 || (i >= 0 && i < j)) {
                        String n = s.substring(pos, i).trim();
                        if (!n.isEmpty()) {
                            tokens.add(n);
                        }
                        tokens.add(s.substring(i, i + 1));
                        pos = i + 1;
                        i = s.indexOf(',', pos);
                    } else {
                        String n = s.substring(pos, j).trim();
                        if (!n.isEmpty()) {
                            tokens.add(n);
                        }
                        tokens.add(s.substring(j, j + 1));
                        pos = j + 1;
                        j = s.indexOf(';', pos);
                    }
                }

                String n = s.substring(pos).trim();
                if (!n.isEmpty()) {
                    tokens.add(n);
                }
            }
            this.tokens.add(tokens);
            return tokens;
        }

        /*
         * Returns next token.
         */
        String nextToken() {
            while (lineNumber <= tokens.size()) {
                List<String> l = tokens.get(lineNumber-1);
                if (index < l.size()) {
                    return l.get(index++);
                } else {
                    lineNumber++;
                    index = 0;
                }
            }
            return null;
        }

        /*
         * Peeks next token.
         */
        String peekToken() {
            int ln = lineNumber;
            int i = index;
            while (ln <= tokens.size()) {
                List<String> l = tokens.get(ln-1);
                if (i < l.size()) {
                    return l.get(i++);
                } else {
                    ln++;
                    i = 0;
                }
            }
            return null;
        }

        Error newError(String msg) {
            if (lineNumber <= tokens.size()) {
                throw new Error(sourceFile + ", line " +
                    lineNumber + ", " + msg + " \"" + lineAt(lineNumber) + "\"");
            } else {
                throw new Error(sourceFile + ", line " + lineNumber + ", " + msg);
            }
        }

        void dump() {
            for (int i = 1; i <= tokens.size(); i++) {
                System.out.format("%d: %s%n", i, lineAt(i));
            }
        }

        private String lineAt(int i) {
            return tokens.get(i-1).stream().collect(Collectors.joining(" "));
        }
    }
}