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.HashMap;
  34 import java.util.HashSet;
  35 import java.util.List;
  36 import java.util.Map;
  37 import java.util.Objects;
  38 import java.util.Set;
  39 import java.util.stream.Collectors;
  40 
  41 /**
  42  * A build tool to extend the module-info.java in the source tree for
  43  * platform-specific exports, uses, and provides and write to the specified
  44  * output file. Injecting platform-specific requires is not supported.
  45  *
  46  * The extra exports, uses, provides can be specified in module-info.java.extra
  47  * files and GenModuleInfoSource will be invoked for each module that has
  48  * module-info.java.extra in the source directory.
  49  */
  50 public class GenModuleInfoSource {
  51     private final static String USAGE =
  52         "Usage: GenModuleInfoSource [option] -o <output file> <module-info-java>\n" +
  53         "Options are:\n" +
  54         "  -exports  <package-name>\n" +
  55         "  -exports  <package-name>/<module-name>\n" +
  56         "  -uses     <service>\n" +
  57         "  -provides <service>/<provider-impl-classname>\n";
  58 
  59     public static void main(String... args) throws Exception {
  60         Path outfile = null;
  61         Path moduleInfoJava = null;
  62         GenModuleInfoSource genModuleInfo = new GenModuleInfoSource();
  63 
  64         // validate input arguments
  65         for (int i = 0; i < args.length; i++){
  66             String option = args[i];
  67             if (option.startsWith("-")) {
  68                 String arg = args[++i];
  69                 if (option.equals("-exports")) {
  70                     int index = arg.indexOf('/');
  71                         if (index > 0) {
  72                             String pn = arg.substring(0, index);
  73                             String mn = arg.substring(index + 1, arg.length());
  74                             genModuleInfo.exportTo(pn, mn);
  75                         } else {
  76                             genModuleInfo.export(arg);
  77                         }
  78                 } else if (option.equals("-uses")) {
  79                     genModuleInfo.use(arg);
  80                 } else if (option.equals("-provides")) {
  81                         int index = arg.indexOf('/');
  82                         if (index <= 0) {
  83                             throw new IllegalArgumentException("invalid -provide argument: " + arg);
  84                         }
  85                         String service = arg.substring(0, index);
  86                         String impl = arg.substring(index + 1, arg.length());
  87                         genModuleInfo.provide(service, impl);
  88                 } else if (option.equals("-o")) {
  89                     outfile = Paths.get(arg);
  90                 } else {
  91                     throw new IllegalArgumentException("invalid option: " + option);
  92                 }
  93             } else if (moduleInfoJava != null) {
  94                 throw new IllegalArgumentException("more than one module-info.java");
  95             } else {
  96                 moduleInfoJava = Paths.get(option);
  97                 if (Files.notExists(moduleInfoJava)) {
  98                     throw new IllegalArgumentException(option + " not exist");
  99                 }
 100             }
 101         }
 102 
 103         if (moduleInfoJava == null || outfile == null) {
 104             System.err.println(USAGE);
 105             System.exit(-1);
 106         }
 107 
 108         // generate new module-info.java
 109         genModuleInfo.generate(moduleInfoJava, outfile);
 110     }
 111 
 112     private final Set<String> exports = new HashSet<>();
 113     private final Map<String, Set<String>> exportsTo = new HashMap<>();
 114     private final Set<String> uses = new HashSet<>();
 115     private final Map<String, Set<String>> provides = new HashMap<>();
 116     GenModuleInfoSource() {
 117     }
 118 
 119     private void export(String p) {
 120         Objects.requireNonNull(p);
 121         if (exports.contains(p) || exportsTo.containsKey(p)) {
 122             throw new RuntimeException("duplicated exports: " + p);
 123         }
 124         exports.add(p);
 125     }
 126     private void exportTo(String p, String mn) {
 127         Objects.requireNonNull(p);
 128         Objects.requireNonNull(mn);
 129         if (exports.contains(p)) {
 130             throw new RuntimeException("unqualified exports already exists: " + p);
 131         }
 132         exportsTo.computeIfAbsent(p, _k -> new HashSet<>()).add(mn);
 133     }
 134 
 135     private void use(String service) {
 136         uses.add(service);
 137     }
 138 
 139     private void provide(String s, String impl) {
 140         provides.computeIfAbsent(s, _k -> new HashSet<>()).add(impl);
 141     }
 142 
 143     private void generate(Path sourcefile, Path outfile) throws IOException {
 144         Path parent = outfile.getParent();
 145         if (parent != null)
 146             Files.createDirectories(parent);
 147 
 148         List<String> lines = Files.readAllLines(sourcefile);
 149         try (BufferedWriter bw = Files.newBufferedWriter(outfile);
 150              PrintWriter writer = new PrintWriter(bw)) {
 151             int lineNumber = 0;
 152             for (String l : lines) {
 153                 lineNumber++;
 154                 String[] s = l.trim().split("\\s+");
 155                 String keyword = s[0].trim();
 156                 int nextIndex = keyword.length();
 157                 String exp = null;
 158                 int n = l.length();
 159                 switch (keyword) {
 160                     case "exports":
 161                         boolean inExportsTo = false;
 162                         // assume package name immediately after exports
 163                         exp = s[1].trim();
 164                         if (s.length >= 3) {
 165                             nextIndex = l.indexOf(exp, nextIndex) + exp.length();
 166                             if (s[2].trim().equals("to")) {
 167                                 inExportsTo = true;
 168                                 n = l.indexOf("to", nextIndex) + "to".length();
 169                             } else {
 170                                 throw new RuntimeException(sourcefile + ", line " +
 171                                     lineNumber + ", is malformed: " + s[2]);
 172                             }
 173                         }
 174 
 175                         // inject the extra targets after "to"
 176                         if (inExportsTo) {
 177                             writer.println(injectExportTargets(exp, l, n));
 178                         } else {
 179                             writer.println(l);
 180                         }
 181                         break;
 182                     case "to":
 183                         if (exp == null) {
 184                             throw new RuntimeException(sourcefile + ", line " +
 185                                 lineNumber + ", is malformed");
 186                         }
 187                         n = l.indexOf("to", nextIndex) + "to".length();
 188                         writer.println(injectExportTargets(exp, l, n));
 189                         break;
 190                     case "}":
 191                         doAugments(writer);
 192                         // fall through
 193                     default:
 194                         writer.println(l);
 195                         // reset exports
 196                         exp = null;
 197                 }
 198             }
 199         }
 200     }
 201 
 202     private String injectExportTargets(String pn, String exp, int pos) {
 203         Set<String> targets = exportsTo.remove(pn);
 204         if (targets != null) {
 205             StringBuilder sb = new StringBuilder();
 206             // inject the extra targets after the given pos
 207             sb.append(exp.substring(0, pos))
 208               .append("\n\t")
 209               .append(targets.stream()
 210                              .collect(Collectors.joining(",", "", ",")))
 211               .append(" /* injected */");
 212             if (pos < exp.length()) {
 213                 // print the remaining statement followed "to"
 214                 sb.append("\n\t")
 215                   .append(exp.substring(pos+1, exp.length()));
 216             }
 217             return sb.toString();
 218         } else {
 219             return exp;
 220         }
 221     }
 222 
 223     private void doAugments(PrintWriter writer) {
 224         if ((exports.size() + exportsTo.size() + uses.size() + provides.size()) == 0)
 225             return;
 226 
 227         writer.println("    // augmented from module-info.java.extra");
 228         exports.stream()
 229             .sorted()
 230             .forEach(e -> writer.format("    exports %s;%n", e));
 231         // remaining injected qualified exports
 232         exportsTo.entrySet().stream()
 233             .sorted(Map.Entry.comparingByKey())
 234             .map(e -> String.format("    exports %s to%n%s;", e.getKey(),
 235                                     e.getValue().stream().sorted()
 236                                         .map(mn -> String.format("        %s", mn))
 237                                         .collect(Collectors.joining(",\n"))))
 238             .forEach(writer::println);
 239         uses.stream().sorted()
 240             .forEach(s -> writer.format("    uses %s;%n", s));
 241         provides.entrySet().stream()
 242             .sorted(Map.Entry.comparingByKey())
 243             .flatMap(e -> e.getValue().stream().sorted()
 244                            .map(impl -> String.format("    provides %s with %s;",
 245                                                       e.getKey(), impl)))
 246             .forEach(writer::println);
 247     }
 248 }