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 * Loads class files from the CLASSPATH. Inspired by sun.tools.ClassPath. 045 */ 046public class ClassPath implements Closeable { 047 048 private abstract static class AbstractPathEntry implements Closeable { 049 050 abstract ClassFile getClassFile(String name, String suffix); 051 052 abstract URL getResource(String name); 053 054 abstract InputStream getResourceAsStream(String name); 055 } 056 057 private abstract static class AbstractZip extends AbstractPathEntry { 058 059 private final ZipFile zipFile; 060 061 AbstractZip(final ZipFile zipFile) { 062 this.zipFile = Objects.requireNonNull(zipFile, "zipFile"); 063 } 064 065 @Override 066 public void close() throws IOException { 067 if (zipFile != null) { 068 zipFile.close(); 069 } 070 071 } 072 073 @Override 074 ClassFile getClassFile(final String name, final String suffix) { 075 final ZipEntry entry = zipFile.getEntry(toEntryName(name, suffix)); 076 077 if (entry == null) { 078 return null; 079 } 080 081 return new ClassFile() { 082 083 @Override 084 public String getBase() { 085 return zipFile.getName(); 086 } 087 088 @Override 089 public InputStream getInputStream() throws IOException { 090 return zipFile.getInputStream(entry); 091 } 092 093 @Override 094 public String getPath() { 095 return entry.toString(); 096 } 097 098 @Override 099 public long getSize() { 100 return entry.getSize(); 101 } 102 103 @Override 104 public long getTime() { 105 return entry.getTime(); 106 } 107 }; 108 } 109 110 @Override 111 URL getResource(final String name) { 112 final ZipEntry entry = zipFile.getEntry(name); 113 try { 114 return entry != null ? new URL("jar:file:" + zipFile.getName() + "!/" + name) : null; 115 } catch (final MalformedURLException e) { 116 return null; 117 } 118 } 119 120 @Override 121 InputStream getResourceAsStream(final String name) { 122 final ZipEntry entry = zipFile.getEntry(name); 123 try { 124 return entry != null ? zipFile.getInputStream(entry) : null; 125 } catch (final IOException e) { 126 return null; 127 } 128 } 129 130 protected abstract String toEntryName(final String name, final String suffix); 131 132 @Override 133 public String toString() { 134 return zipFile.getName(); 135 } 136 137 } 138 139 /** 140 * Contains information about file/ZIP entry of the Java class. 141 */ 142 public interface ClassFile { 143 144 /** 145 * @return base path of found class, i.e. class is contained relative to that path, which may either denote a directory, 146 * or zip file 147 */ 148 String getBase(); 149 150 /** 151 * @return input stream for class file. 152 * @throws IOException if an I/O error occurs. 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) { 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) { 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 Objects.toString(resolved.getFileName(), null); 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 this.modules = modularRuntimeImage.list(path).stream().map(JrtModule::new).toArray(JrtModule[]::new); 358 } 359 360 @Override 361 public void close() throws IOException { 362 if (modules != null) { 363 // don't use a for each loop to avoid creating an iterator for the GC to collect. 364 for (final JrtModule module : modules) { 365 module.close(); 366 } 367 } 368 if (modularRuntimeImage != null) { 369 modularRuntimeImage.close(); 370 } 371 } 372 373 @Override 374 ClassFile getClassFile(final String name, final String suffix) { 375 // don't use a for each loop to avoid creating an iterator for the GC to collect. 376 for (final JrtModule module : modules) { 377 final ClassFile classFile = module.getClassFile(name, suffix); 378 if (classFile != null) { 379 return classFile; 380 } 381 } 382 return null; 383 } 384 385 @Override 386 URL getResource(final String name) { 387 // don't use a for each loop to avoid creating an iterator for the GC to collect. 388 for (final JrtModule module : modules) { 389 final URL url = module.getResource(name); 390 if (url != null) { 391 return url; 392 } 393 } 394 return null; 395 } 396 397 @Override 398 InputStream getResourceAsStream(final String name) { 399 // don't use a for each loop to avoid creating an iterator for the GC to collect. 400 for (final JrtModule module : modules) { 401 final InputStream inputStream = module.getResourceAsStream(name); 402 if (inputStream != null) { 403 return inputStream; 404 } 405 } 406 return null; 407 } 408 409 @Override 410 public String toString() { 411 return Arrays.toString(modules); 412 } 413 414 } 415 416 private static class Module extends AbstractZip { 417 418 Module(final ZipFile zip) { 419 super(zip); 420 } 421 422 @Override 423 protected String toEntryName(final String name, final String suffix) { 424 return "classes/" + packageToFolder(name) + suffix; 425 } 426 427 } 428 429 private static final FilenameFilter ARCHIVE_FILTER = (dir, name) -> { 430 name = name.toLowerCase(Locale.ENGLISH); 431 return name.endsWith(".zip") || name.endsWith(".jar"); 432 }; 433 434 private static final FilenameFilter MODULES_FILTER = (dir, name) -> { 435 name = name.toLowerCase(Locale.ENGLISH); 436 return name.endsWith(".jmod"); 437 }; 438 439 public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath(getClassPath()); 440 441 private static void addJdkModules(final String javaHome, final List<String> list) { 442 String modulesPath = System.getProperty("java.modules.path"); 443 if (modulesPath == null || modulesPath.trim().isEmpty()) { 444 // Default to looking in JAVA_HOME/jmods 445 modulesPath = javaHome + File.separator + "jmods"; 446 } 447 final File modulesDir = new File(modulesPath); 448 if (modulesDir.exists()) { 449 final String[] modules = modulesDir.list(MODULES_FILTER); 450 if (modules != null) { 451 for (final String module : modules) { 452 list.add(modulesDir.getPath() + File.separatorChar + module); 453 } 454 } 455 } 456 } 457 458 /** 459 * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path", 460 * "java.ext.dirs" 461 * 462 * @return class path as used by default by BCEL 463 */ 464 // @since 6.0 no longer final 465 public static String getClassPath() { 466 final String classPathProp = System.getProperty("java.class.path"); 467 final String bootClassPathProp = System.getProperty("sun.boot.class.path"); 468 final String extDirs = System.getProperty("java.ext.dirs"); 469 // System.out.println("java.version = " + System.getProperty("java.version")); 470 // System.out.println("java.class.path = " + classPathProp); 471 // System.out.println("sun.boot.class.path=" + bootClassPathProp); 472 // System.out.println("java.ext.dirs=" + extDirs); 473 final String javaHome = System.getProperty("java.home"); 474 final List<String> list = new ArrayList<>(); 475 476 // Starting in JRE 9, .class files are in the modules directory. Add them to the path. 477 final Path modulesPath = Paths.get(javaHome).resolve("lib/modules"); 478 if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) { 479 list.add(modulesPath.toAbsolutePath().toString()); 480 } 481 // Starting in JDK 9, .class files are in the jmods directory. Add them to the path. 482 addJdkModules(javaHome, list); 483 484 getPathComponents(classPathProp, list); 485 getPathComponents(bootClassPathProp, list); 486 final List<String> dirs = new ArrayList<>(); 487 getPathComponents(extDirs, dirs); 488 for (final String d : dirs) { 489 final File ext_dir = new File(d); 490 final String[] extensions = ext_dir.list(ARCHIVE_FILTER); 491 if (extensions != null) { 492 for (final String extension : extensions) { 493 list.add(ext_dir.getPath() + File.separatorChar + extension); 494 } 495 } 496 } 497 498 final StringBuilder buf = new StringBuilder(); 499 String separator = ""; 500 for (final String path : list) { 501 buf.append(separator); 502 separator = File.pathSeparator; 503 buf.append(path); 504 } 505 return buf.toString().intern(); 506 } 507 508 private static void getPathComponents(final String path, final List<String> list) { 509 if (path != null) { 510 final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator); 511 while (tokenizer.hasMoreTokens()) { 512 final String name = tokenizer.nextToken(); 513 final File file = new File(name); 514 if (file.exists()) { 515 list.add(name); 516 } 517 } 518 } 519 } 520 521 static String packageToFolder(final String name) { 522 return name.replace('.', '/'); 523 } 524 525 private final String classPath; 526 527 private ClassPath parent; 528 529 private final AbstractPathEntry[] paths; 530 531 /** 532 * Search for classes in CLASSPATH. 533 * 534 * @deprecated Use SYSTEM_CLASS_PATH constant 535 */ 536 @Deprecated 537 public ClassPath() { 538 this(getClassPath()); 539 } 540 541 public ClassPath(final ClassPath parent, final String classPath) { 542 this(classPath); 543 this.parent = parent; 544 } 545 546 /** 547 * Search for classes in given path. 548 * 549 * @param classPath 550 */ 551 @SuppressWarnings("resource") 552 public ClassPath(final String classPath) { 553 this.classPath = classPath; 554 final List<AbstractPathEntry> list = new ArrayList<>(); 555 for (final StringTokenizer tokenizer = new StringTokenizer(classPath, File.pathSeparator); tokenizer.hasMoreTokens();) { 556 final String path = tokenizer.nextToken(); 557 if (!path.isEmpty()) { 558 final File file = new File(path); 559 try { 560 if (file.exists()) { 561 if (file.isDirectory()) { 562 list.add(new Dir(path)); 563 } else if (path.endsWith(".jmod")) { 564 list.add(new Module(new ZipFile(file))); 565 } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) { 566 list.add(new JrtModules(ModularRuntimeImage.MODULES_PATH)); 567 } else { 568 list.add(new Jar(new ZipFile(file))); 569 } 570 } 571 } catch (final IOException e) { 572 if (path.endsWith(".zip") || path.endsWith(".jar")) { 573 System.err.println("CLASSPATH component " + file + ": " + e); 574 } 575 } 576 } 577 } 578 paths = new AbstractPathEntry[list.size()]; 579 list.toArray(paths); 580 } 581 582 @Override 583 public void close() throws IOException { 584 if (paths != null) { 585 for (final AbstractPathEntry path : paths) { 586 path.close(); 587 } 588 } 589 590 } 591 592 @Override 593 public boolean equals(final Object o) { 594 if (o instanceof ClassPath) { 595 final ClassPath cp = (ClassPath) o; 596 return classPath.equals(cp.toString()); 597 } 598 return false; 599 } 600 601 /** 602 * @param name fully qualified file name, e.g. java/lang/String 603 * @return byte array for class 604 * @throws IOException if an I/O error occurs. 605 */ 606 public byte[] getBytes(final String name) throws IOException { 607 return getBytes(name, ".class"); 608 } 609 610 /** 611 * @param name fully qualified file name, e.g. java/lang/String 612 * @param suffix file name ends with suffix, e.g. .java 613 * @return byte array for file on class path 614 * @throws IOException if an I/O error occurs. 615 */ 616 public byte[] getBytes(final String name, final String suffix) throws IOException { 617 DataInputStream dis = null; 618 try (InputStream inputStream = getInputStream(name, suffix)) { 619 if (inputStream == null) { 620 throw new IOException("Couldn't find: " + name + suffix); 621 } 622 dis = new DataInputStream(inputStream); 623 final byte[] bytes = new byte[inputStream.available()]; 624 dis.readFully(bytes); 625 return bytes; 626 } finally { 627 if (dis != null) { 628 dis.close(); 629 } 630 } 631 } 632 633 /** 634 * @param name fully qualified class name, e.g. java.lang.String 635 * @return input stream for class 636 * @throws IOException if an I/O error occurs. 637 */ 638 public ClassFile getClassFile(final String name) throws IOException { 639 return getClassFile(name, ".class"); 640 } 641 642 /** 643 * @param name fully qualified file name, e.g. java/lang/String 644 * @param suffix file name ends with suff, e.g. .java 645 * @return class file for the java class 646 * @throws IOException if an I/O error occurs. 647 */ 648 public ClassFile getClassFile(final String name, final String suffix) throws IOException { 649 ClassFile cf = null; 650 651 if (parent != null) { 652 cf = parent.getClassFileInternal(name, suffix); 653 } 654 655 if (cf == null) { 656 cf = getClassFileInternal(name, suffix); 657 } 658 659 if (cf != null) { 660 return cf; 661 } 662 663 throw new IOException("Couldn't find: " + name + suffix); 664 } 665 666 private ClassFile getClassFileInternal(final String name, final String suffix) { 667 for (final AbstractPathEntry path : paths) { 668 final ClassFile cf = path.getClassFile(name, suffix); 669 if (cf != null) { 670 return cf; 671 } 672 } 673 return null; 674 } 675 676 /** 677 * @param name fully qualified class name, e.g. java.lang.String 678 * @return input stream for class 679 * @throws IOException if an I/O error occurs. 680 */ 681 public InputStream getInputStream(final String name) throws IOException { 682 return getInputStream(packageToFolder(name), ".class"); 683 } 684 685 /** 686 * Return stream for class or resource on CLASSPATH. 687 * 688 * @param name fully qualified file name, e.g. java/lang/String 689 * @param suffix file name ends with suff, e.g. .java 690 * @return input stream for file on class path 691 * @throws IOException if an I/O error occurs. 692 */ 693 public InputStream getInputStream(final String name, final String suffix) throws IOException { 694 InputStream inputStream = null; 695 try { 696 inputStream = getClass().getClassLoader().getResourceAsStream(name + suffix); // may return null 697 } catch (final Exception ignored) { 698 // ignored 699 } 700 if (inputStream != null) { 701 return inputStream; 702 } 703 return getClassFile(name, suffix).getInputStream(); 704 } 705 706 /** 707 * @param name name of file to search for, e.g. java/lang/String.java 708 * @return full (canonical) path for file 709 * @throws IOException if an I/O error occurs. 710 */ 711 public String getPath(String name) throws IOException { 712 final int index = name.lastIndexOf('.'); 713 String suffix = ""; 714 if (index > 0) { 715 suffix = name.substring(index); 716 name = name.substring(0, index); 717 } 718 return getPath(name, suffix); 719 } 720 721 /** 722 * @param name name of file to search for, e.g. java/lang/String 723 * @param suffix file name suffix, e.g. .java 724 * @return full (canonical) path for file, if it exists 725 * @throws IOException if an I/O error occurs. 726 */ 727 public String getPath(final String name, final String suffix) throws IOException { 728 return getClassFile(name, suffix).getPath(); 729 } 730 731 /** 732 * @param name fully qualified resource name, e.g. java/lang/String.class 733 * @return URL supplying the resource, or null if no resource with that name. 734 * @since 6.0 735 */ 736 public URL getResource(final String name) { 737 for (final AbstractPathEntry path : paths) { 738 URL url; 739 if ((url = path.getResource(name)) != null) { 740 return url; 741 } 742 } 743 return null; 744 } 745 746 /** 747 * @param name fully qualified resource name, e.g. java/lang/String.class 748 * @return InputStream supplying the resource, or null if no resource with that name. 749 * @since 6.0 750 */ 751 public InputStream getResourceAsStream(final String name) { 752 for (final AbstractPathEntry path : paths) { 753 InputStream is; 754 if ((is = path.getResourceAsStream(name)) != null) { 755 return is; 756 } 757 } 758 return null; 759 } 760 761 /** 762 * @param name fully qualified resource name, e.g. java/lang/String.class 763 * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name. 764 * @since 6.0 765 */ 766 public Enumeration<URL> getResources(final String name) { 767 final Vector<URL> results = new Vector<>(); 768 for (final AbstractPathEntry path : paths) { 769 URL url; 770 if ((url = path.getResource(name)) != null) { 771 results.add(url); 772 } 773 } 774 return results.elements(); 775 } 776 777 @Override 778 public int hashCode() { 779 if (parent != null) { 780 return classPath.hashCode() + parent.hashCode(); 781 } 782 return classPath.hashCode(); 783 } 784 785 /** 786 * @return used class path string 787 */ 788 @Override 789 public String toString() { 790 if (parent != null) { 791 return parent + File.pathSeparator + classPath; 792 } 793 return classPath; 794 } 795}