001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 * 017 */ 018package org.apache.bcel.util; 019 020import java.io.Closeable; 021import java.io.DataInputStream; 022import java.io.File; 023import java.io.FileInputStream; 024import java.io.FilenameFilter; 025import java.io.IOException; 026import java.io.InputStream; 027import java.net.MalformedURLException; 028import java.net.URL; 029import java.nio.file.Files; 030import java.nio.file.Path; 031import java.nio.file.Paths; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.Enumeration; 035import java.util.List; 036import java.util.Locale; 037import java.util.Objects; 038import java.util.StringTokenizer; 039import java.util.Vector; 040import java.util.zip.ZipEntry; 041import java.util.zip.ZipFile; 042 043/** 044 * Responsible for loading (class) files from the CLASSPATH. Inspired by sun.tools.ClassPath. 045 * 046 */ 047public class ClassPath implements Closeable { 048 049 private abstract static class AbstractPathEntry implements Closeable { 050 051 abstract ClassFile getClassFile(String name, String suffix) throws IOException; 052 053 abstract URL getResource(String name); 054 055 abstract InputStream getResourceAsStream(String name); 056 } 057 058 private abstract static class AbstractZip extends AbstractPathEntry { 059 060 private final ZipFile zipFile; 061 062 AbstractZip(final ZipFile zipFile) { 063 this.zipFile = Objects.requireNonNull(zipFile, "zipFile"); 064 } 065 066 @Override 067 public void close() throws IOException { 068 if (zipFile != null) { 069 zipFile.close(); 070 } 071 072 } 073 074 @Override 075 ClassFile getClassFile(final String name, final String suffix) throws IOException { 076 final ZipEntry entry = zipFile.getEntry(toEntryName(name, suffix)); 077 078 if (entry == null) { 079 return null; 080 } 081 082 return new ClassFile() { 083 084 @Override 085 public String getBase() { 086 return zipFile.getName(); 087 } 088 089 @Override 090 public InputStream getInputStream() throws IOException { 091 return zipFile.getInputStream(entry); 092 } 093 094 @Override 095 public String getPath() { 096 return entry.toString(); 097 } 098 099 @Override 100 public long getSize() { 101 return entry.getSize(); 102 } 103 104 @Override 105 public long getTime() { 106 return entry.getTime(); 107 } 108 }; 109 } 110 111 @Override 112 URL getResource(final String name) { 113 final ZipEntry entry = zipFile.getEntry(name); 114 try { 115 return entry != null ? new URL("jar:file:" + zipFile.getName() + "!/" + name) : null; 116 } catch (final MalformedURLException e) { 117 return null; 118 } 119 } 120 121 @Override 122 InputStream getResourceAsStream(final String name) { 123 final ZipEntry entry = zipFile.getEntry(name); 124 try { 125 return entry != null ? zipFile.getInputStream(entry) : null; 126 } catch (final IOException e) { 127 return null; 128 } 129 } 130 131 protected abstract String toEntryName(final String name, final String suffix); 132 133 @Override 134 public String toString() { 135 return zipFile.getName(); 136 } 137 138 } 139 140 /** 141 * Contains information about file/ZIP entry of the Java class. 142 */ 143 public interface ClassFile { 144 145 /** 146 * @return base path of found class, i.e. class is contained relative to that path, which may either denote a 147 * directory, or zip file 148 */ 149 String getBase(); 150 151 /** 152 * @return input stream for class file. 153 */ 154 InputStream getInputStream() throws IOException; 155 156 /** 157 * @return canonical path to class file. 158 */ 159 String getPath(); 160 161 /** 162 * @return size of class file. 163 */ 164 long getSize(); 165 166 /** 167 * @return modification time of class file. 168 */ 169 long getTime(); 170 } 171 172 private static class Dir extends AbstractPathEntry { 173 174 private final String dir; 175 176 Dir(final String d) { 177 dir = d; 178 } 179 180 @Override 181 public void close() throws IOException { 182 // Nothing to do 183 184 } 185 186 @Override 187 ClassFile getClassFile(final String name, final String suffix) throws IOException { 188 final File file = new File(dir + File.separatorChar + name.replace('.', File.separatorChar) + suffix); 189 return file.exists() ? new ClassFile() { 190 191 @Override 192 public String getBase() { 193 return dir; 194 } 195 196 @Override 197 public InputStream getInputStream() throws IOException { 198 return new FileInputStream(file); 199 } 200 201 @Override 202 public String getPath() { 203 try { 204 return file.getCanonicalPath(); 205 } catch (final IOException e) { 206 return null; 207 } 208 } 209 210 @Override 211 public long getSize() { 212 return file.length(); 213 } 214 215 @Override 216 public long getTime() { 217 return file.lastModified(); 218 } 219 } : null; 220 } 221 222 @Override 223 URL getResource(final String name) { 224 // Resource specification uses '/' whatever the platform 225 final File file = toFile(name); 226 try { 227 return file.exists() ? file.toURI().toURL() : null; 228 } catch (final MalformedURLException e) { 229 return null; 230 } 231 } 232 233 @Override 234 InputStream getResourceAsStream(final String name) { 235 // Resource specification uses '/' whatever the platform 236 final File file = toFile(name); 237 try { 238 return file.exists() ? new FileInputStream(file) : null; 239 } catch (final IOException e) { 240 return null; 241 } 242 } 243 244 private File toFile(final String name) { 245 return new File(dir + File.separatorChar + name.replace('/', File.separatorChar)); 246 } 247 248 @Override 249 public String toString() { 250 return dir; 251 } 252 } 253 254 private static class Jar extends AbstractZip { 255 256 Jar(final ZipFile zip) { 257 super(zip); 258 } 259 260 @Override 261 protected String toEntryName(final String name, final String suffix) { 262 return packageToFolder(name) + suffix; 263 } 264 265 } 266 267 private static class JrtModule extends AbstractPathEntry { 268 269 private final Path modulePath; 270 271 public JrtModule(final Path modulePath) { 272 this.modulePath = Objects.requireNonNull(modulePath, "modulePath"); 273 } 274 275 @Override 276 public void close() throws IOException { 277 // Nothing to do. 278 279 } 280 281 @Override 282 ClassFile getClassFile(final String name, final String suffix) throws IOException { 283 final Path resolved = modulePath.resolve(packageToFolder(name) + suffix); 284 if (Files.exists(resolved)) { 285 return new ClassFile() { 286 287 @Override 288 public String getBase() { 289 return resolved.getFileName().toString(); 290 } 291 292 @Override 293 public InputStream getInputStream() throws IOException { 294 return Files.newInputStream(resolved); 295 } 296 297 @Override 298 public String getPath() { 299 return resolved.toString(); 300 } 301 302 @Override 303 public long getSize() { 304 try { 305 return Files.size(resolved); 306 } catch (final IOException e) { 307 return 0; 308 } 309 } 310 311 @Override 312 public long getTime() { 313 try { 314 return Files.getLastModifiedTime(resolved).toMillis(); 315 } catch (final IOException e) { 316 return 0; 317 } 318 } 319 }; 320 } 321 return null; 322 } 323 324 @Override 325 URL getResource(final String name) { 326 final Path resovled = modulePath.resolve(name); 327 try { 328 return Files.exists(resovled) ? new URL("jrt:" + modulePath + "/" + name) : null; 329 } catch (final MalformedURLException e) { 330 return null; 331 } 332 } 333 334 @Override 335 InputStream getResourceAsStream(final String name) { 336 try { 337 return Files.newInputStream(modulePath.resolve(name)); 338 } catch (final IOException e) { 339 return null; 340 } 341 } 342 343 @Override 344 public String toString() { 345 return modulePath.toString(); 346 } 347 348 } 349 350 private static class JrtModules extends AbstractPathEntry { 351 352 private final ModularRuntimeImage modularRuntimeImage; 353 private final JrtModule[] modules; 354 355 public JrtModules(final String path) throws IOException { 356 this.modularRuntimeImage = new ModularRuntimeImage(); 357 final List<Path> list = modularRuntimeImage.list(path); 358 this.modules = new JrtModule[list.size()]; 359 for (int i = 0; i < modules.length; i++) { 360 modules[i] = new JrtModule(list.get(i)); 361 } 362 } 363 364 @Override 365 public void close() throws IOException { 366 if (modules != null) { 367 // don't use a for each loop to avoid creating an iterator for the GC to collect. 368 for (int i = 0; i < modules.length; i++) { 369 modules[i].close(); 370 } 371 } 372 if (modularRuntimeImage != null) { 373 modularRuntimeImage.close(); 374 } 375 } 376 377 @Override 378 ClassFile getClassFile(final String name, final String suffix) throws IOException { 379 // don't use a for each loop to avoid creating an iterator for the GC to collect. 380 for (int i = 0; i < modules.length; i++) { 381 final ClassFile classFile = modules[i].getClassFile(name, suffix); 382 if (classFile != null) { 383 return classFile; 384 } 385 } 386 return null; 387 } 388 389 @Override 390 URL getResource(final String name) { 391 // don't use a for each loop to avoid creating an iterator for the GC to collect. 392 for (int i = 0; i < modules.length; i++) { 393 final URL url = modules[i].getResource(name); 394 if (url != null) { 395 return url; 396 } 397 } 398 return null; 399 } 400 401 @Override 402 InputStream getResourceAsStream(final String name) { 403 // don't use a for each loop to avoid creating an iterator for the GC to collect. 404 for (int i = 0; i < modules.length; i++) { 405 final InputStream inputStream = modules[i].getResourceAsStream(name); 406 if (inputStream != null) { 407 return inputStream; 408 } 409 } 410 return null; 411 } 412 413 @Override 414 public String toString() { 415 return Arrays.toString(modules); 416 } 417 418 } 419 420 private static class Module extends AbstractZip { 421 422 Module(final ZipFile zip) { 423 super(zip); 424 } 425 426 @Override 427 protected String toEntryName(final String name, final String suffix) { 428 return "classes/" + packageToFolder(name) + suffix; 429 } 430 431 } 432 433 private static final FilenameFilter ARCHIVE_FILTER = (dir, name) -> { 434 name = name.toLowerCase(Locale.ENGLISH); 435 return name.endsWith(".zip") || name.endsWith(".jar"); 436 }; 437 438 private static final FilenameFilter MODULES_FILTER = (dir, name) -> { 439 name = name.toLowerCase(Locale.ENGLISH); 440 return name.endsWith(".jmod"); 441 }; 442 443 public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath(getClassPath()); 444 445 private static void addJdkModules(final String javaHome, final List<String> list) { 446 String modulesPath = System.getProperty("java.modules.path"); 447 if (modulesPath == null || modulesPath.trim().isEmpty()) { 448 // Default to looking in JAVA_HOME/jmods 449 modulesPath = javaHome + File.separator + "jmods"; 450 } 451 final File modulesDir = new File(modulesPath); 452 if (modulesDir.exists()) { 453 final String[] modules = modulesDir.list(MODULES_FILTER); 454 for (int i = 0; i < modules.length; i++) { 455 list.add(modulesDir.getPath() + File.separatorChar + modules[i]); 456 } 457 } 458 } 459 460 /** 461 * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path", 462 * "java.ext.dirs" 463 * 464 * @return class path as used by default by BCEL 465 */ 466 // @since 6.0 no longer final 467 public static String getClassPath() { 468 final String classPathProp = System.getProperty("java.class.path"); 469 final String bootClassPathProp = System.getProperty("sun.boot.class.path"); 470 final String extDirs = System.getProperty("java.ext.dirs"); 471 // System.out.println("java.version = " + System.getProperty("java.version")); 472 // System.out.println("java.class.path = " + classPathProp); 473 // System.out.println("sun.boot.class.path=" + bootClassPathProp); 474 // System.out.println("java.ext.dirs=" + extDirs); 475 final String javaHome = System.getProperty("java.home"); 476 final List<String> list = new ArrayList<>(); 477 478 // Starting in JRE 9, .class files are in the modules directory. Add them to the path. 479 final Path modulesPath = Paths.get(javaHome).resolve("lib/modules"); 480 if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) { 481 list.add(modulesPath.toAbsolutePath().toString()); 482 } 483 // Starting in JDK 9, .class files are in the jmods directory. Add them to the path. 484 addJdkModules(javaHome, list); 485 486 getPathComponents(classPathProp, list); 487 getPathComponents(bootClassPathProp, list); 488 final List<String> dirs = new ArrayList<>(); 489 getPathComponents(extDirs, dirs); 490 for (final String d : dirs) { 491 final File ext_dir = new File(d); 492 final String[] extensions = ext_dir.list(ARCHIVE_FILTER); 493 if (extensions != null) { 494 for (final String extension : extensions) { 495 list.add(ext_dir.getPath() + File.separatorChar + extension); 496 } 497 } 498 } 499 500 final StringBuilder buf = new StringBuilder(); 501 String separator = ""; 502 for (final String path : list) { 503 buf.append(separator); 504 separator = File.pathSeparator; 505 buf.append(path); 506 } 507 return buf.toString().intern(); 508 } 509 510 private static void getPathComponents(final String path, final List<String> list) { 511 if (path != null) { 512 final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator); 513 while (tokenizer.hasMoreTokens()) { 514 final String name = tokenizer.nextToken(); 515 final File file = new File(name); 516 if (file.exists()) { 517 list.add(name); 518 } 519 } 520 } 521 } 522 523 static String packageToFolder(final String name) { 524 return name.replace('.', '/'); 525 } 526 527 private final String classPath; 528 529 private ClassPath parent; 530 531 private final AbstractPathEntry[] paths; 532 533 /** 534 * Search for classes in CLASSPATH. 535 * 536 * @deprecated Use SYSTEM_CLASS_PATH constant 537 */ 538 @Deprecated 539 public ClassPath() { 540 this(getClassPath()); 541 } 542 543 public ClassPath(final ClassPath parent, final String classPath) { 544 this(classPath); 545 this.parent = parent; 546 } 547 548 /** 549 * Search for classes in given path. 550 * 551 * @param classPath 552 */ 553 @SuppressWarnings("resource") 554 public ClassPath(final String classPath) { 555 this.classPath = classPath; 556 final List<AbstractPathEntry> list = new ArrayList<>(); 557 for (final StringTokenizer tokenizer = new StringTokenizer(classPath, File.pathSeparator); tokenizer 558 .hasMoreTokens();) { 559 final String path = tokenizer.nextToken(); 560 if (!path.isEmpty()) { 561 final File file = new File(path); 562 try { 563 if (file.exists()) { 564 if (file.isDirectory()) { 565 list.add(new Dir(path)); 566 } else if (path.endsWith(".jmod")) { 567 list.add(new Module(new ZipFile(file))); 568 } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) { 569 list.add(new JrtModules(ModularRuntimeImage.MODULES_PATH)); 570 } else { 571 list.add(new Jar(new ZipFile(file))); 572 } 573 } 574 } catch (final IOException e) { 575 if (path.endsWith(".zip") || path.endsWith(".jar")) { 576 System.err.println("CLASSPATH component " + file + ": " + e); 577 } 578 } 579 } 580 } 581 paths = new AbstractPathEntry[list.size()]; 582 list.toArray(paths); 583 } 584 585 @Override 586 public void close() throws IOException { 587 if (paths != null) { 588 for (final AbstractPathEntry path : paths) { 589 path.close(); 590 } 591 } 592 593 } 594 595 @Override 596 public boolean equals(final Object o) { 597 if (o instanceof ClassPath) { 598 final ClassPath cp = (ClassPath) o; 599 return classPath.equals(cp.toString()); 600 } 601 return false; 602 } 603 604 /** 605 * @return byte array for class 606 */ 607 public byte[] getBytes(final String name) throws IOException { 608 return getBytes(name, ".class"); 609 } 610 611 /** 612 * @param name 613 * fully qualified file name, e.g. java/lang/String 614 * @param suffix 615 * file name ends with suffix, e.g. .java 616 * @return byte array for file on class path 617 */ 618 public byte[] getBytes(final String name, final String suffix) throws IOException { 619 DataInputStream dis = null; 620 try (InputStream inputStream = getInputStream(name, suffix)) { 621 if (inputStream == null) { 622 throw new IOException("Couldn't find: " + name + suffix); 623 } 624 dis = new DataInputStream(inputStream); 625 final byte[] bytes = new byte[inputStream.available()]; 626 dis.readFully(bytes); 627 return bytes; 628 } finally { 629 if (dis != null) { 630 dis.close(); 631 } 632 } 633 } 634 635 /** 636 * @param name 637 * fully qualified class name, e.g. java.lang.String 638 * @return input stream for class 639 */ 640 public ClassFile getClassFile(final String name) throws IOException { 641 return getClassFile(name, ".class"); 642 } 643 644 /** 645 * @param name 646 * fully qualified file name, e.g. java/lang/String 647 * @param suffix 648 * file name ends with suff, e.g. .java 649 * @return class file for the java class 650 */ 651 public ClassFile getClassFile(final String name, final String suffix) throws IOException { 652 ClassFile cf = null; 653 654 if (parent != null) { 655 cf = parent.getClassFileInternal(name, suffix); 656 } 657 658 if (cf == null) { 659 cf = getClassFileInternal(name, suffix); 660 } 661 662 if (cf != null) { 663 return cf; 664 } 665 666 throw new IOException("Couldn't find: " + name + suffix); 667 } 668 669 private ClassFile getClassFileInternal(final String name, final String suffix) throws IOException { 670 671 for (final AbstractPathEntry path : paths) { 672 final ClassFile cf = path.getClassFile(name, suffix); 673 674 if (cf != null) { 675 return cf; 676 } 677 } 678 679 return null; 680 } 681 682 /** 683 * @param name 684 * fully qualified class name, e.g. java.lang.String 685 * @return input stream for class 686 */ 687 public InputStream getInputStream(final String name) throws IOException { 688 return getInputStream(packageToFolder(name), ".class"); 689 } 690 691 /** 692 * Return stream for class or resource on CLASSPATH. 693 * 694 * @param name 695 * fully qualified file name, e.g. java/lang/String 696 * @param suffix 697 * file name ends with suff, e.g. .java 698 * @return input stream for file on class path 699 */ 700 public InputStream getInputStream(final String name, final String suffix) throws IOException { 701 InputStream inputStream = null; 702 try { 703 inputStream = getClass().getClassLoader().getResourceAsStream(name + suffix); // may return null 704 } catch (final Exception e) { 705 // ignored 706 } 707 if (inputStream != null) { 708 return inputStream; 709 } 710 return getClassFile(name, suffix).getInputStream(); 711 } 712 713 /** 714 * @param name 715 * name of file to search for, e.g. java/lang/String.java 716 * @return full (canonical) path for file 717 */ 718 public String getPath(String name) throws IOException { 719 final int index = name.lastIndexOf('.'); 720 String suffix = ""; 721 if (index > 0) { 722 suffix = name.substring(index); 723 name = name.substring(0, index); 724 } 725 return getPath(name, suffix); 726 } 727 728 /** 729 * @param name 730 * name of file to search for, e.g. java/lang/String 731 * @param suffix 732 * file name suffix, e.g. .java 733 * @return full (canonical) path for file, if it exists 734 */ 735 public String getPath(final String name, final String suffix) throws IOException { 736 return getClassFile(name, suffix).getPath(); 737 } 738 739 /** 740 * @param name 741 * fully qualified resource name, e.g. java/lang/String.class 742 * @return URL supplying the resource, or null if no resource with that name. 743 * @since 6.0 744 */ 745 public URL getResource(final String name) { 746 for (final AbstractPathEntry path : paths) { 747 URL url; 748 if ((url = path.getResource(name)) != null) { 749 return url; 750 } 751 } 752 return null; 753 } 754 755 /** 756 * @param name 757 * fully qualified resource name, e.g. java/lang/String.class 758 * @return InputStream supplying the resource, or null if no resource with that name. 759 * @since 6.0 760 */ 761 public InputStream getResourceAsStream(final String name) { 762 for (final AbstractPathEntry path : paths) { 763 InputStream is; 764 if ((is = path.getResourceAsStream(name)) != null) { 765 return is; 766 } 767 } 768 return null; 769 } 770 771 /** 772 * @param name 773 * fully qualified resource name, e.g. java/lang/String.class 774 * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name. 775 * @since 6.0 776 */ 777 public Enumeration<URL> getResources(final String name) { 778 final Vector<URL> results = new Vector<>(); 779 for (final AbstractPathEntry path : paths) { 780 URL url; 781 if ((url = path.getResource(name)) != null) { 782 results.add(url); 783 } 784 } 785 return results.elements(); 786 } 787 788 @Override 789 public int hashCode() { 790 if (parent != null) { 791 return classPath.hashCode() + parent.hashCode(); 792 } 793 return classPath.hashCode(); 794 } 795 796 /** 797 * @return used class path string 798 */ 799 @Override 800 public String toString() { 801 if (parent != null) { 802 return parent + File.pathSeparator + classPath; 803 } 804 return classPath; 805 } 806}