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 }