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