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.IOException;
  28 import java.nio.file.Files;
  29 import java.nio.file.Path;
  30 import java.util.List;
  31 import java.util.function.Supplier;
  32 import java.util.regex.Pattern;
  33 import java.util.stream.Stream;
  34 
  35 import build.tools.module.Module.Builder;
  36 
  37 /**
  38  * Source reader of module-info.java
  39  */
  40 public class ModuleInfoReader {
  41     private final Path sourcefile;
  42     private final Builder builder;
  43     private ModuleInfoReader(Path file) {
  44         this.sourcefile = file;
  45         this.builder = new Builder();
  46     }
  47 
  48     public static Builder builder(Path file) throws IOException {
  49         ModuleInfoReader reader = new ModuleInfoReader(file);
  50         reader.readFile();
  51         return reader.builder;
  52     }
  53 
  54     /**
  55      * Reads the source file.
  56      */
  57     void readFile() throws IOException {
  58         List<String> lines = Files.readAllLines(sourcefile);
  59         boolean done = false;
  60         int lineNumber = 0;
  61         boolean inBlockComment = false;
  62         boolean inRequires = false;
  63         boolean reexports = false;
  64         boolean inProvides = false;
  65         boolean inWith = false;
  66         String serviceIntf = null;
  67         String providerClass = null;
  68         boolean inUses = false;
  69         boolean inExports = false;
  70         boolean inExportsTo = false;
  71         String qualifiedExports = null;
  72         Counter counter = new Counter();
  73 
  74         for (String line : lines) {
  75             lineNumber++;
  76             if (inBlockComment) {
  77                 int c = line.indexOf("*/");
  78                 if (c >= 0) {
  79                     line = line.substring(c + 2, line.length());
  80                     inBlockComment = false;
  81                 } else {
  82                     // skip lines until end of comment block
  83                     continue;
  84                 }
  85             }
  86             inBlockComment = beginBlockComment(line);
  87 
  88             line = trimComment(line).trim();
  89             // ignore empty lines
  90             if (line.length() == 0) {
  91                 continue;
  92             }
  93             String values;
  94             if (inRequires || inExports | inUses | (inWith && providerClass == null)) {
  95                 values = line;
  96             } else {
  97                 String[] s = line.split("\\s+");
  98                 String keyword = s[0].trim();
  99                 int nextIndex = keyword.length();
 100                 switch (keyword) {
 101                     case "module":
 102                         if (s.length != 3 || !s[2].trim().equals("{")) {
 103                             throw new RuntimeException(sourcefile + ", line " +
 104                                     lineNumber + ", is malformed");
 105                         }
 106                         builder.name(s[1].trim());
 107                         continue;  // next line
 108                     case "requires":
 109                         inRequires = true;
 110                         counter.numRequires++;
 111                         if (s.length >= 2) {
 112                             String ss = s[1].trim();
 113                             if (ss.equals("public")) {
 114                                 nextIndex = line.indexOf(ss) + ss.length();
 115                                 reexports = true;
 116                             }
 117                         }
 118                         break;
 119                     case "exports":
 120                         inExports = true;
 121                         inExportsTo = false;
 122                         counter.numExports++;
 123                         qualifiedExports = null;
 124                         if (s.length >= 3) {
 125                             qualifiedExports = s[1].trim();
 126                             nextIndex = line.indexOf(qualifiedExports, nextIndex)
 127                                             + qualifiedExports.length();
 128                             if (s[2].trim().equals("to")) {
 129                                 inExportsTo = true;
 130                                 nextIndex = line.indexOf("to", nextIndex) + "to".length();
 131                             } else {
 132                                 throw new RuntimeException(sourcefile + ", line " +
 133                                         lineNumber + ", is malformed: " + s[2]);
 134                             }
 135                         }
 136                         break;
 137                     case "to":
 138                         if (!inExports || qualifiedExports == null) {
 139                             throw new RuntimeException(sourcefile + ", line " +
 140                                     lineNumber + ", is malformed");
 141                         }
 142                         inExportsTo = true;
 143                         break;
 144                     case "uses":
 145                         inUses = true;
 146                         counter.numUses++;
 147                         break;
 148                     case "provides":
 149                         inProvides = true;
 150                         inWith = false;
 151                         counter.numProvides++;
 152                         serviceIntf = null;
 153                         providerClass = null;
 154                         if (s.length >= 2) {
 155                             serviceIntf = s[1].trim();
 156                             nextIndex = line.indexOf(serviceIntf) + serviceIntf.length();
 157                         }
 158                         if (s.length >= 3) {
 159                             if (s[2].trim().equals("with")) {
 160                                 inWith = true;
 161                                 nextIndex = line.indexOf("with") + "with".length();
 162                             } else {
 163                                 throw new RuntimeException(sourcefile + ", line " +
 164                                         lineNumber + ", is malformed: " + s[2]);
 165                             }
 166                         }
 167                         break;
 168                     case "with":
 169                         if (!inProvides || serviceIntf == null) {
 170                             throw new RuntimeException(sourcefile + ", line " +
 171                                     lineNumber + ", is malformed");
 172                         }
 173                         inWith = true;
 174                         nextIndex = line.indexOf("with") + "with".length();
 175                         break;
 176                     case "}":
 177                         counter.validate(builder);
 178                         done = true;
 179                         continue;  // next line
 180                     default:
 181                         throw new RuntimeException(sourcefile + ", \"" +
 182                                 keyword + "\" on line " +
 183                                 lineNumber + ", is not recognized");
 184                 }
 185                 values = line.substring(nextIndex, line.length()).trim();
 186             }
 187 
 188             int len = values.length();
 189             if (len == 0) {
 190                 continue;  // next line
 191             }
 192             char lastchar = values.charAt(len - 1);
 193             if (lastchar != ',' && lastchar != ';') {
 194                 throw new RuntimeException(sourcefile + ", line " +
 195                         lineNumber + ", is malformed:" +
 196                         " ',' or ';' is missing.");
 197             }
 198 
 199             values = values.substring(0, len - 1).trim();
 200             // parse the values specified for a keyword specified
 201             for (String s : values.split(",")) {
 202                 s = s.trim();
 203                 if (s.length() > 0) {
 204                     if (inRequires) {
 205                         if (builder.requires.contains(s)) {
 206                             throw new RuntimeException(sourcefile + ", line "
 207                                     + lineNumber + " duplicated requires: \"" + s + "\"");
 208                         }
 209                         builder.require(s, reexports);
 210                     } else if (inExports) {
 211                         if (!inExportsTo && qualifiedExports == null) {
 212                             builder.export(s);
 213                         } else {
 214                             builder.exportTo(qualifiedExports, s);
 215                         }
 216                     } else if (inUses) {
 217                         builder.use(s);
 218                     } else if (inProvides) {
 219                         if (!inWith) {
 220                             serviceIntf = s;
 221                         } else {
 222                             providerClass = s;
 223                             builder.provide(serviceIntf, providerClass);
 224                         }
 225                     }
 226                 }
 227             }
 228             if (lastchar == ';') {
 229                 inRequires = false;
 230                 reexports = false;
 231                 inExports = false;
 232                 inExportsTo = false;
 233                 inProvides = false;
 234                 inWith = false;
 235                 inUses = false;
 236             }
 237         }
 238 
 239         if (inBlockComment) {
 240             throw new RuntimeException(sourcefile + ", line " +
 241                     lineNumber + ", missing \"*/\" to end a block comment");
 242         }
 243         if (!done) {
 244             throw new RuntimeException(sourcefile + ", line " +
 245                     lineNumber + ", missing \"}\" to end module definition" +
 246                     " for \"" + builder + "\"");
 247         }
 248         return;
 249     }
 250 
 251     // the naming convention for the module names without dashes
 252     private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("[\\w\\.\\*_$/]+");
 253     private static boolean beginBlockComment(String line) {
 254         int pos = 0;
 255         while (pos >= 0 && pos < line.length()) {
 256             int c = line.indexOf("/*", pos);
 257             if (c < 0) {
 258                 return false;
 259             }
 260 
 261             if (c > 0 && !Character.isWhitespace(line.charAt(c - 1))) {
 262                 return false;
 263             }
 264 
 265             int c1 = line.indexOf("//", pos);
 266             if (c1 >= 0 && c1 < c) {
 267                 return false;
 268             }
 269 
 270             int c2 = line.indexOf("*/", c + 2);
 271             if (c2 < 0) {
 272                 return true;
 273             }
 274             pos = c + 2;
 275         }
 276         return false;
 277     }
 278     private static String trimComment(String line) {
 279         StringBuilder sb = new StringBuilder();
 280 
 281         int pos = 0;
 282         while (pos >= 0 && pos < line.length()) {
 283             int c1 = line.indexOf("//", pos);
 284             if (c1 > 0 && !Character.isWhitespace(line.charAt(c1 - 1))) {
 285                 // not a comment
 286                 c1 = -1;
 287             }
 288 
 289             int c2 = line.indexOf("/*", pos);
 290             if (c2 > 0 && !Character.isWhitespace(line.charAt(c2 - 1))) {
 291                 // not a comment
 292                 c2 = -1;
 293             }
 294 
 295             int c = line.length();
 296             int n = line.length();
 297             if (c1 >= 0 || c2 >= 0) {
 298                 if (c1 >= 0) {
 299                     c = c1;
 300                 }
 301                 if (c2 >= 0 && c2 < c) {
 302                     c = c2;
 303                 }
 304                 int c3 = line.indexOf("*/", c2 + 2);
 305                 if (c == c2 && c3 > c2) {
 306                     n = c3 + 2;
 307                 }
 308             }
 309             if (c > 0) {
 310                 if (sb.length() > 0) {
 311                     // add a whitespace if multiple comments on one line
 312                     sb.append(" ");
 313                 }
 314                 sb.append(line.substring(pos, c));
 315             }
 316             pos = n;
 317         }
 318         return sb.toString();
 319     }
 320 
 321 
 322     static class Counter {
 323         int numRequires;
 324         int numExports;
 325         int numUses;
 326         int numProvides;
 327 
 328         void validate(Builder builder) {
 329             assertEquals("requires", numRequires, builder.requires.size(),
 330                          () -> builder.requires.stream()
 331                                       .map(Module.Dependence::toString));
 332             assertEquals("exports", numExports, builder.exports.size(),
 333                          () -> builder.exports.entrySet().stream()
 334                                       .map(e -> "exports " + e.getKey() + " to " + e.getValue()));
 335             assertEquals("uses", numUses, builder.uses.size(),
 336                          () -> builder.uses.stream());
 337             assertEquals("provides", numProvides,
 338                          (int)builder.provides.values().stream()
 339                                      .flatMap(s -> s.stream())
 340                                      .count(),
 341                          () -> builder.provides.entrySet().stream()
 342                                       .map(e -> "provides " + e.getKey() + " with " + e.getValue()));
 343         }
 344 
 345         private static void assertEquals(String msg, int expected, int got,
 346                                          Supplier<Stream<String>> supplier) {
 347             if (expected != got){
 348                 System.err.println("ERROR: mismatched " + msg +
 349                         " expected: " + expected + " got: " + got );
 350                 supplier.get().sorted()
 351                         .forEach(System.err::println);
 352                 throw new AssertionError("mismatched " + msg +
 353                         " expected: " + expected + " got: " + got + " ");
 354             }
 355         }
 356     }
 357 }