1 /* 2 * Copyright (c) 2009, 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. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package com.sun.classanalyzer; 24 25 import java.io.BufferedReader; 26 import java.io.FileInputStream; 27 import java.io.IOException; 28 import java.io.InputStreamReader; 29 import java.util.ArrayList; 30 import java.util.Collection; 31 import java.util.LinkedList; 32 import java.util.List; 33 import java.util.Set; 34 import java.util.TreeSet; 35 import java.util.regex.Pattern; 36 37 import com.sun.classanalyzer.Module.RequiresModule; 38 import java.util.LinkedHashMap; 39 import java.util.Map; 40 41 /** 42 * 43 * @author Mandy Chung 44 */ 45 public class ModuleConfig { 46 47 private final Set<String> roots; 48 private final Set<String> includes; 49 private final Set<String> permits; 50 private final Map<String, RequiresModule> requires; 51 private final Filter filter; 52 private List<String> members; 53 private String mainClass; 54 final String module; 55 56 ModuleConfig(String name) throws IOException { 57 this(name, null); 58 } 59 60 ModuleConfig(String name, String mainClass) throws IOException { 61 this.module = name; 62 this.roots = new TreeSet<String>(); 63 this.includes = new TreeSet<String>(); 64 this.permits = new TreeSet<String>(); 65 this.requires = new LinkedHashMap<String, RequiresModule>(); 66 this.filter = new Filter(this); 67 this.mainClass = mainClass; 68 } 69 70 List<String> members() { 71 if (members == null) { 72 members = new LinkedList<String>(); 73 74 for (String s : includes) { 75 if (!s.contains("*") && Module.findModule(s) != null) { 76 // module member 77 members.add(s); 78 } 79 } 80 } 81 return members; 82 } 83 84 Set<String> permits() { 85 return permits; 86 } 87 88 Collection<RequiresModule> requires() { 89 return requires.values(); 90 } 91 92 String mainClass() { 93 return mainClass; 94 } 95 96 boolean matchesRoot(String name) { 97 for (String pattern : roots) { 98 if (matches(name, pattern)) { 99 return true; 100 } 101 } 102 return false; 103 } 104 105 boolean matchesIncludes(String name) { 106 for (String pattern : includes) { 107 if (matches(name, pattern)) { 108 return true; 109 } 110 } 111 return false; 112 } 113 114 boolean isExcluded(String name) { 115 return filter.isExcluded(name); 116 } 117 118 boolean matchesPackage(String packageName, String pattern) { 119 int pos = pattern.lastIndexOf('.'); 120 String pkg = pos > 0 ? pattern.substring(0, pos) : "<unnamed>"; 121 return packageName.equals(pkg); 122 } 123 124 boolean matches(String name, String pattern) { 125 if (pattern.contains("**") && !pattern.endsWith("**")) { 126 throw new UnsupportedOperationException("Not yet implemented"); 127 } 128 129 String javaName = name; 130 131 boolean isResourceFile = name.indexOf('/') >= 0; 132 if (isResourceFile) { 133 // it's a resource file; convert the name as a java 134 javaName = name.replace('/', '.'); 135 } 136 if (pattern.indexOf('/') < 0) { 137 // if the pattern doesn't contain '/ 138 return matchesJavaName(javaName, pattern); 139 } else { 140 if (isResourceFile) { 141 // the pattern is for matching resource file 142 return matchesNameWithSlash(name, pattern); 143 } else { 144 return false; 145 } 146 } 147 } 148 149 boolean matchesJavaName(String name, String pattern) { 150 int pos = name.lastIndexOf('.'); 151 String packageName = pos > 0 ? name.substring(0, pos) : "<unnamed>"; 152 if (pattern.endsWith("**")) { 153 String p = pattern.substring(0, pattern.length() - 2); 154 return name.startsWith(p); 155 } else if (pattern.endsWith("*") && pattern.indexOf('*') == pattern.lastIndexOf('*')) { 156 if (matchesPackage(packageName, pattern)) { 157 // package name has to be exact match 158 String p = pattern.substring(0, pattern.length() - 1); 159 return name.startsWith(p); 160 } else { 161 return false; 162 } 163 } else if (pattern.contains("*")) { 164 String basename = pos > 0 ? name.substring(pos + 1, name.length()) : name; 165 pos = pattern.indexOf('*'); 166 String prefix = pattern.substring(0, pos); 167 String suffix = pattern.substring(pos + 1, pattern.length()); 168 if (name.startsWith(prefix) && matchesPackage(packageName, prefix)) { 169 // package name has to be exact match 170 if (suffix.contains("*")) { 171 return name.matches(convertToRegex(pattern)); 172 } else { 173 return basename.endsWith(suffix); 174 } 175 } else { 176 // we don't support wildcard be used in the package name 177 return false; 178 } 179 } else { 180 // exact match or inner class 181 return name.equals(pattern) || name.startsWith(pattern + "$"); 182 } 183 } 184 185 boolean matchesNameWithSlash(String name, String pattern) { 186 if (pattern.endsWith("**")) { 187 String p = pattern.substring(0, pattern.length() - 2); 188 return name.startsWith(p); 189 } else if (pattern.contains("*")) { 190 int pos = pattern.indexOf('*'); 191 String prefix = pattern.substring(0, pos); 192 String suffix = pattern.substring(pos + 1, pattern.length()); 193 String tail = name.substring(pos, name.length()); 194 195 if (!name.startsWith(prefix)) { 196 // prefix has to exact match 197 return false; 198 } 199 200 if (pattern.indexOf('*') == pattern.lastIndexOf('*')) { 201 // exact match prefix with no '/' in the tail string 202 String wildcard = tail.substring(0, tail.length() - suffix.length()); 203 return tail.indexOf('/') < 0 && tail.endsWith(suffix); 204 } 205 206 if (suffix.contains("*")) { 207 return matchesNameWithSlash(tail, suffix); 208 } else { 209 // tail ends with the suffix while no '/' in the wildcard matched string 210 String any = tail.substring(0, tail.length() - suffix.length()); 211 return tail.endsWith(suffix) && any.indexOf('/') < 0; 212 } 213 } else { 214 // exact match 215 return name.equals(pattern); 216 } 217 } 218 219 private String convertToRegex(String pattern) { 220 StringBuilder sb = new StringBuilder(); 221 int i = 0; 222 int index = 0; 223 int plen = pattern.length(); 224 while (i < plen) { 225 char p = pattern.charAt(i); 226 if (p == '*') { 227 sb.append("(").append(pattern.substring(index, i)).append(")"); 228 if (i + 1 < plen && pattern.charAt(i + 1) == '*') { 229 sb.append(".*"); 230 index = i + 2; 231 } else { 232 sb.append("[^\\.]*"); 233 index = i + 1; 234 } 235 } 236 i++; 237 } 238 if (index < plen) { 239 sb.append("(").append(pattern.substring(index, plen)).append(")"); 240 } 241 return sb.toString(); 242 } 243 244 static class Filter { 245 246 final ModuleConfig config; 247 final Set<String> exclude = new TreeSet<String>(); 248 final Set<String> allow = new TreeSet<String>(); 249 250 Filter(ModuleConfig config) { 251 this.config = config; 252 } 253 254 Filter exclude(String pattern) { 255 exclude.add(pattern); 256 return this; 257 } 258 259 Filter allow(String pattern) { 260 allow.add(pattern); 261 return this; 262 } 263 264 String allowedBy(String name) { 265 String allowedBy = null; 266 for (String pattern : allow) { 267 if (config.matches(name, pattern)) { 268 if (name.equals(pattern)) { 269 return pattern; // exact match 270 } 271 if (allowedBy == null) { 272 allowedBy = pattern; 273 } else { 274 if (pattern.length() > allowedBy.length()) { 275 allowedBy = pattern; 276 } 277 } 278 } 279 } 280 return allowedBy; 281 } 282 283 String excludedBy(String name) { 284 String allowedBy = allowedBy(name); 285 String excludedBy = null; 286 287 if (allowedBy != null && name.equals(allowedBy)) { 288 return null; // exact match 289 } 290 for (String pattern : exclude) { 291 if (config.matches(name, pattern)) { 292 // not matched by allowed rule or exact match 293 if (allowedBy == null || name.equals(pattern)) { 294 return pattern; 295 } 296 if (excludedBy == null) { 297 excludedBy = pattern; 298 } else { 299 if (pattern.length() > excludedBy.length()) { 300 excludedBy = pattern; 301 } 302 } 303 } 304 } 305 return excludedBy; 306 } 307 308 boolean isExcluded(String name) { 309 String allowedBy = allowedBy(name); 310 String excludedBy = excludedBy(name); 311 312 if (excludedBy == null) { 313 return false; 314 } 315 // not matched by allowed rule or exact match 316 if (allowedBy == null || name.equals(excludedBy)) { 317 return true; 318 } 319 320 if (allowedBy == null) { 321 return true; 322 } 323 if (allowedBy != null && 324 excludedBy.length() > allowedBy.length()) { 325 return true; 326 } 327 return false; 328 } 329 } 330 331 private static String trimComment(String line) { 332 StringBuilder sb = new StringBuilder(); 333 334 int pos = 0; 335 while (pos >= 0 && pos < line.length()) { 336 int c1 = line.indexOf("//", pos); 337 if (c1 > 0 && !Character.isWhitespace(line.charAt(c1 - 1))) { 338 // not a comment 339 c1 = -1; 340 } 341 342 int c2 = line.indexOf("/*", pos); 343 if (c2 > 0 && !Character.isWhitespace(line.charAt(c2 - 1))) { 344 // not a comment 345 c2 = -1; 346 } 347 348 int c = line.length(); 349 int n = line.length(); 350 if (c1 >= 0 || c2 >= 0) { 351 if (c1 >= 0) { 352 c = c1; 353 } 354 if (c2 >= 0 && c2 < c) { 355 c = c2; 356 } 357 int c3 = line.indexOf("*/", c2 + 2); 358 if (c == c2 && c3 > c2) { 359 n = c3 + 2; 360 } 361 } 362 if (c > 0) { 363 if (sb.length() > 0) { 364 // add a whitespace if multiple comments on one line 365 sb.append(" "); 366 } 367 sb.append(line.substring(pos, c)); 368 } 369 pos = n; 370 } 371 return sb.toString(); 372 } 373 374 private static boolean beginBlockComment(String line) { 375 int pos = 0; 376 while (pos >= 0 && pos < line.length()) { 377 int c = line.indexOf("/*", pos); 378 if (c < 0) { 379 return false; 380 } 381 382 if (c > 0 && !Character.isWhitespace(line.charAt(c - 1))) { 383 return false; 384 } 385 386 int c1 = line.indexOf("//", pos); 387 if (c1 >= 0 && c1 < c) { 388 return false; 389 } 390 391 int c2 = line.indexOf("*/", c + 2); 392 if (c2 < 0) { 393 return true; 394 } 395 pos = c + 2; 396 } 397 return false; 398 } 399 // TODO: we shall remove "-" from the regex once we define 400 // the naming convention for the module names without dashes 401 static final Pattern classNamePattern = Pattern.compile("[\\w\\.\\*_$-/]+"); 402 403 static List<ModuleConfig> readConfigurationFile(String file) throws IOException { 404 List<ModuleConfig> result = new ArrayList<ModuleConfig>(); 405 // parse configuration file 406 FileInputStream in = new FileInputStream(file); 407 try { 408 BufferedReader reader = new BufferedReader(new InputStreamReader(in)); 409 String line; 410 411 int lineNumber = 0; 412 boolean inRoots = false; 413 boolean inIncludes = false; 414 boolean inAllows = false; 415 boolean inExcludes = false; 416 boolean inPermits = false; 417 boolean inRequires = false; 418 boolean optional = false; 419 boolean reexport = false; 420 boolean local = false; 421 422 boolean inBlockComment = false; 423 ModuleConfig config = null; 424 425 while ((line = reader.readLine()) != null) { 426 lineNumber++; 427 428 if (inBlockComment) { 429 int c = line.indexOf("*/"); 430 if (c >= 0) { 431 line = line.substring(c + 2, line.length()); 432 inBlockComment = false; 433 } else { 434 // skip lines until end of comment block 435 continue; 436 } 437 } 438 439 inBlockComment = beginBlockComment(line); 440 441 line = trimComment(line).trim(); 442 // ignore empty lines 443 if (line.length() == 0) { 444 continue; 445 } 446 447 String values; 448 if (inRoots || inIncludes || inExcludes || inAllows || inPermits || inRequires) { 449 values = line; 450 } else { 451 String[] s = line.split("\\s+"); 452 String keyword = s[0].trim(); 453 int nextIndex = keyword.length(); 454 if (keyword.equals("module")) { 455 if (s.length != 3 || !s[2].trim().equals("{")) { 456 throw new RuntimeException(file + ", line " + 457 lineNumber + ", is malformed"); 458 } 459 config = new ModuleConfig(s[1].trim()); 460 result.add(config); 461 // switch to a new module; so reset the flags 462 inRoots = false; 463 inIncludes = false; 464 inExcludes = false; 465 inAllows = false; 466 inRequires = false; 467 inPermits = false; 468 continue; 469 } else if (keyword.equals("class")) { 470 if (s.length != 2 || !s[1].trim().endsWith(";")) { 471 throw new RuntimeException(file + ", line " + 472 lineNumber + ", is malformed"); 473 } 474 config.mainClass = s[1].substring(0, s[1].length() - 1); 475 continue; 476 } else if (keyword.equals("roots")) { 477 inRoots = true; 478 } else if (keyword.equals("include")) { 479 inIncludes = true; 480 } else if (keyword.equals("exclude")) { 481 inExcludes = true; 482 } else if (keyword.equals("allow")) { 483 inAllows = true; 484 } else if (keyword.equals("permits")) { 485 inPermits = true; 486 } else if (keyword.equals("requires")) { 487 inRequires = true; 488 optional = false; 489 reexport = false; 490 local = false; 491 for (int i=1; i < s.length; i++) { 492 String ss = s[i].trim(); 493 if (ss.equals("public")) { 494 reexport = true; 495 nextIndex = line.indexOf(ss) + ss.length(); 496 } else if (ss.equals("optional")) { 497 optional = true; 498 nextIndex = line.indexOf(ss) + ss.length(); 499 } else if (ss.equals("local")) { 500 local = true; 501 nextIndex = line.indexOf(ss) + ss.length(); 502 } else { 503 break; 504 } 505 } 506 } else if (keyword.equals("}")) { 507 if (config == null || s.length != 1) { 508 throw new RuntimeException(file + ", line " + 509 lineNumber + ", is malformed"); 510 } else { 511 // end of a module 512 config = null; 513 continue; 514 } 515 } else { 516 throw new RuntimeException(file + ", \"" + keyword + "\" on line " + 517 lineNumber + ", is not recognized"); 518 } 519 520 values = line.substring(nextIndex, line.length()).trim(); 521 } 522 523 if (config == null) { 524 throw new RuntimeException(file + ", module not specified"); 525 } 526 527 int len = values.length(); 528 if (len == 0) { 529 continue; 530 } 531 char lastchar = values.charAt(len - 1); 532 if (lastchar != ',' && lastchar != ';') { 533 throw new RuntimeException(file + ", line " + 534 lineNumber + ", is malformed:" + 535 " ',' or ';' is missing."); 536 } 537 538 values = values.substring(0, len - 1); 539 // parse the values specified for a keyword specified 540 for (String s : values.split(",")) { 541 s = s.trim(); 542 if (s.length() > 0) { 543 if (!classNamePattern.matcher(s).matches()) { 544 throw new RuntimeException(file + ", line " + 545 lineNumber + ", is malformed: \"" + s + "\""); 546 } 547 if (inRoots) { 548 config.roots.add(s); 549 } else if (inIncludes) { 550 config.includes.add(s); 551 } else if (inExcludes) { 552 config.filter.exclude(s); 553 } else if (inAllows) { 554 config.filter.allow(s); 555 } else if (inPermits) { 556 config.permits.add(s); 557 } else if (inRequires) { 558 if (config.requires.containsKey(s)) { 559 throw new RuntimeException(file + ", line " + 560 lineNumber + " duplicated requires: \"" + s + "\""); 561 } 562 boolean isBootModule = s.equals("jdk.boot"); 563 if (!local && isBootModule) { 564 throw new RuntimeException(file + ", line " + 565 lineNumber + " requires: \"" + s + "\" must be local"); 566 } 567 RequiresModule rm = new RequiresModule(s, optional, reexport, local); 568 config.requires.put(s, rm); 569 } 570 } 571 } 572 if (lastchar == ';') { 573 inRoots = false; 574 inIncludes = false; 575 inExcludes = false; 576 inAllows = false; 577 inPermits = false; 578 inRequires = false; 579 } 580 } 581 582 if (inBlockComment) { 583 throw new RuntimeException(file + ", line " + 584 lineNumber + ", missing \"*/\" to end a block comment"); 585 } 586 if (config != null) { 587 throw new RuntimeException(file + ", line " + 588 lineNumber + ", missing \"}\" to end module definition" + 589 " for \"" + config.module + "\""); 590 } 591 } finally { 592 in.close(); 593 } 594 595 return result; 596 } 597 598 private String format(String keyword, Collection<String> values) { 599 if (values.size() == 0) { 600 return ""; 601 } 602 603 StringBuilder sb = new StringBuilder(); 604 String format = "%4s%-9s"; 605 String spaces = String.format(format, "", ""); 606 sb.append(String.format(format, "", keyword)); 607 int count = 0; 608 for (String s : values) { 609 if (count > 0) { 610 sb.append(",\n").append(spaces); 611 } else if (count++ > 0) { 612 sb.append(", "); 613 } 614 sb.append(s); 615 } 616 if (count > 0) { 617 sb.append(";\n"); 618 } 619 return sb.toString(); 620 } 621 622 @Override 623 public String toString() { 624 StringBuilder sb = new StringBuilder(); 625 sb.append("module " + module).append(" {\n"); 626 sb.append(format("include", includes)); 627 sb.append(format("root", roots)); 628 sb.append(format("allow", filter.allow)); 629 sb.append(format("exclude", filter.exclude)); 630 Set<String> reqs = new TreeSet<String>(); 631 for (RequiresModule rm : requires.values()) { 632 reqs.add(rm.toString()); 633 } 634 sb.append(format("requires", reqs)); 635 sb.append(format("permits", permits)); 636 sb.append("}\n"); 637 return sb.toString(); 638 } 639 }