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