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 }