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 */ 017package org.apache.bcel.util; 018 019import java.io.Closeable; 020import java.io.DataInputStream; 021import java.io.File; 022import java.io.FileInputStream; 023import java.io.FilenameFilter; 024import java.io.IOException; 025import java.io.InputStream; 026import java.net.MalformedURLException; 027import java.net.URL; 028import java.nio.file.Files; 029import java.nio.file.Path; 030import java.nio.file.Paths; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Enumeration; 034import java.util.List; 035import java.util.Locale; 036import java.util.Objects; 037import java.util.StringTokenizer; 038import java.util.Vector; 039import java.util.stream.Collectors; 040import java.util.zip.ZipEntry; 041import java.util.zip.ZipFile; 042 043import org.apache.bcel.classfile.JavaClass; 044import org.apache.bcel.classfile.Utility; 045import org.apache.commons.lang3.SystemProperties; 046 047/** 048 * Loads class files from the CLASSPATH. Inspired by sun.tools.ClassPath. 049 */ 050public class ClassPath implements Closeable { 051 052 private abstract static class AbstractPathEntry implements Closeable { 053 054 abstract ClassFile getClassFile(String name, String suffix); 055 056 abstract URL getResource(String name); 057 058 abstract InputStream getResourceAsStream(String name); 059 } 060 061 private abstract static class AbstractZip extends AbstractPathEntry { 062 063 private final ZipFile zipFile; 064 065 AbstractZip(final ZipFile zipFile) { 066 this.zipFile = Objects.requireNonNull(zipFile, "zipFile"); 067 } 068 069 @Override 070 public void close() throws IOException { 071 if (zipFile != null) { 072 zipFile.close(); 073 } 074 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(final String name, final 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 * @return base path of found class, i.e. class is contained relative to that path, which may either denote a directory, 150 * or ZIP file 151 */ 152 String getBase(); 153 154 /** 155 * @return input stream for class file. 156 * @throws IOException if an I/O error occurs. 157 */ 158 InputStream getInputStream() throws IOException; 159 160 /** 161 * @return canonical path to class file. 162 */ 163 String getPath(); 164 165 /** 166 * @return size of class file. 167 */ 168 long getSize(); 169 170 /** 171 * @return modification time of class file. 172 */ 173 long getTime(); 174 } 175 176 private static final class Dir extends AbstractPathEntry { 177 178 private final String dir; 179 180 Dir(final String d) { 181 dir = d; 182 } 183 184 @Override 185 public void close() throws IOException { 186 // Nothing to do 187 188 } 189 190 @Override 191 ClassFile getClassFile(final String name, final String suffix) { 192 final File file = new File(dir + File.separatorChar + name.replace('.', File.separatorChar) + suffix); 193 return file.exists() ? new ClassFile() { 194 195 @Override 196 public String getBase() { 197 return dir; 198 } 199 200 @Override 201 public InputStream getInputStream() throws IOException { 202 return new FileInputStream(file); 203 } 204 205 @Override 206 public String getPath() { 207 try { 208 return file.getCanonicalPath(); 209 } catch (final IOException e) { 210 return null; 211 } 212 } 213 214 @Override 215 public long getSize() { 216 return file.length(); 217 } 218 219 @Override 220 public long getTime() { 221 return file.lastModified(); 222 } 223 } : null; 224 } 225 226 @Override 227 URL getResource(final String name) { 228 // Resource specification uses '/' whatever the platform 229 final File file = toFile(name); 230 try { 231 return file.exists() ? file.toURI().toURL() : null; 232 } catch (final MalformedURLException e) { 233 return null; 234 } 235 } 236 237 @Override 238 InputStream getResourceAsStream(final String name) { 239 // Resource specification uses '/' whatever the platform 240 final File file = toFile(name); 241 try { 242 return file.exists() ? new FileInputStream(file) : null; 243 } catch (final IOException e) { 244 return null; 245 } 246 } 247 248 private File toFile(final String name) { 249 return new File(dir + File.separatorChar + name.replace('/', File.separatorChar)); 250 } 251 252 @Override 253 public String toString() { 254 return dir; 255 } 256 } 257 258 private static final class Jar extends AbstractZip { 259 260 Jar(final ZipFile zip) { 261 super(zip); 262 } 263 264 @Override 265 protected String toEntryName(final String name, final String suffix) { 266 return Utility.packageToPath(name) + suffix; 267 } 268 269 } 270 271 private static final class JrtModule extends AbstractPathEntry { 272 273 private final Path modulePath; 274 275 public JrtModule(final Path modulePath) { 276 this.modulePath = Objects.requireNonNull(modulePath, "modulePath"); 277 } 278 279 @Override 280 public void close() throws IOException { 281 // Nothing to do. 282 283 } 284 285 @Override 286 ClassFile getClassFile(final String name, final String suffix) { 287 final Path resolved = modulePath.resolve(Utility.packageToPath(name) + suffix); 288 if (Files.exists(resolved)) { 289 return new ClassFile() { 290 291 @Override 292 public String getBase() { 293 return Objects.toString(resolved.getFileName(), null); 294 } 295 296 @Override 297 public InputStream getInputStream() throws IOException { 298 return Files.newInputStream(resolved); 299 } 300 301 @Override 302 public String getPath() { 303 return resolved.toString(); 304 } 305 306 @Override 307 public long getSize() { 308 try { 309 return Files.size(resolved); 310 } catch (final IOException e) { 311 return 0; 312 } 313 } 314 315 @Override 316 public long getTime() { 317 try { 318 return Files.getLastModifiedTime(resolved).toMillis(); 319 } catch (final IOException e) { 320 return 0; 321 } 322 } 323 }; 324 } 325 return null; 326 } 327 328 @Override 329 URL getResource(final String name) { 330 final Path resovled = modulePath.resolve(name); 331 try { 332 return Files.exists(resovled) ? new URL("jrt:" + modulePath + "/" + name) : null; 333 } catch (final MalformedURLException e) { 334 return null; 335 } 336 } 337 338 @Override 339 InputStream getResourceAsStream(final String name) { 340 try { 341 return Files.newInputStream(modulePath.resolve(name)); 342 } catch (final IOException e) { 343 return null; 344 } 345 } 346 347 @Override 348 public String toString() { 349 return modulePath.toString(); 350 } 351 352 } 353 354 private static final class JrtModules extends AbstractPathEntry { 355 356 private final ModularRuntimeImage modularRuntimeImage; 357 private final JrtModule[] modules; 358 359 public JrtModules(final String path) throws IOException { 360 this.modularRuntimeImage = new ModularRuntimeImage(); 361 this.modules = modularRuntimeImage.list(path).stream().map(JrtModule::new).toArray(JrtModule[]::new); 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 (final JrtModule module : modules) { 369 module.close(); 370 } 371 } 372 if (modularRuntimeImage != null) { 373 modularRuntimeImage.close(); 374 } 375 } 376 377 @Override 378 ClassFile getClassFile(final String name, final String suffix) { 379 // don't use a for each loop to avoid creating an iterator for the GC to collect. 380 for (final JrtModule module : modules) { 381 final ClassFile classFile = module.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 (final JrtModule module : modules) { 393 final URL url = module.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 (final JrtModule module : modules) { 405 final InputStream inputStream = module.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 final 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/" + Utility.packageToPath(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(org.apache.bcel.classfile.Module.EXTENSION); 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 if (modules != null) { 455 for (final String module : modules) { 456 list.add(modulesDir.getPath() + File.separatorChar + module); 457 } 458 } 459 } 460 } 461 462 /** 463 * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path", 464 * "java.ext.dirs" 465 * 466 * @return class path as used by default by BCEL 467 */ 468 // @since 6.0 no longer final 469 public static String getClassPath() { 470 final String classPathProp = SystemProperties.getJavaClassPath(); 471 final String bootClassPathProp = System.getProperty("sun.boot.class.path"); 472 final String extDirs = SystemProperties.getJavaExtDirs(); 473 // System.out.println("java.version = " + System.getProperty("java.version")); 474 // System.out.println("java.class.path = " + classPathProp); 475 // System.out.println("sun.boot.class.path=" + bootClassPathProp); 476 // System.out.println("java.ext.dirs=" + extDirs); 477 final String javaHome = SystemProperties.getJavaHome(); 478 final List<String> list = new ArrayList<>(); 479 480 // Starting in JRE 9, .class files are in the modules directory. Add them to the path. 481 final Path modulesPath = Paths.get(javaHome).resolve("lib/modules"); 482 if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) { 483 list.add(modulesPath.toAbsolutePath().toString()); 484 } 485 // Starting in JDK 9, .class files are in the jmods directory. Add them to the path. 486 addJdkModules(javaHome, list); 487 488 getPathComponents(classPathProp, list); 489 getPathComponents(bootClassPathProp, list); 490 final List<String> dirs = new ArrayList<>(); 491 getPathComponents(extDirs, dirs); 492 for (final String d : dirs) { 493 final File extDir = new File(d); 494 final String[] extensions = extDir.list(ARCHIVE_FILTER); 495 if (extensions != null) { 496 for (final String extension : extensions) { 497 list.add(extDir.getPath() + File.separatorChar + extension); 498 } 499 } 500 } 501 502 return list.stream().collect(Collectors.joining(File.pathSeparator)); 503 } 504 505 private static void getPathComponents(final String path, final List<String> list) { 506 if (path != null) { 507 final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator); 508 while (tokenizer.hasMoreTokens()) { 509 final String name = tokenizer.nextToken(); 510 final File file = new File(name); 511 if (file.exists()) { 512 list.add(name); 513 } 514 } 515 } 516 } 517 518 private final String classPathString; 519 520 private final ClassPath parent; 521 522 private final List<AbstractPathEntry> paths; 523 524 /** 525 * Search for classes in CLASSPATH. 526 * 527 * @deprecated Use SYSTEM_CLASS_PATH constant 528 */ 529 @Deprecated 530 public ClassPath() { 531 this(getClassPath()); 532 } 533 534 @SuppressWarnings("resource") 535 public ClassPath(final ClassPath parent, final String classPathString) { 536 this.parent = parent; 537 this.classPathString = Objects.requireNonNull(classPathString, "classPathString"); 538 this.paths = new ArrayList<>(); 539 for (final StringTokenizer tokenizer = new StringTokenizer(classPathString, File.pathSeparator); tokenizer.hasMoreTokens();) { 540 final String path = tokenizer.nextToken(); 541 if (!path.isEmpty()) { 542 final File file = new File(path); 543 try { 544 if (file.exists()) { 545 if (file.isDirectory()) { 546 paths.add(new Dir(path)); 547 } else if (path.endsWith(org.apache.bcel.classfile.Module.EXTENSION)) { 548 paths.add(new Module(new ZipFile(file))); 549 } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) { 550 paths.add(new JrtModules(ModularRuntimeImage.MODULES_PATH)); 551 } else { 552 paths.add(new Jar(new ZipFile(file))); 553 } 554 } 555 } catch (final IOException e) { 556 if (path.endsWith(".zip") || path.endsWith(".jar")) { 557 System.err.println("CLASSPATH component " + file + ": " + e); 558 } 559 } 560 } 561 } 562 } 563 564 /** 565 * Search for classes in given path. 566 * 567 * @param classPath 568 */ 569 public ClassPath(final String classPath) { 570 this(null, classPath); 571 } 572 573 @Override 574 public void close() throws IOException { 575 for (final AbstractPathEntry path : paths) { 576 path.close(); 577 } 578 } 579 580 @Override 581 public boolean equals(final Object obj) { 582 if (this == obj) { 583 return true; 584 } 585 if (obj == null) { 586 return false; 587 } 588 if (getClass() != obj.getClass()) { 589 return false; 590 } 591 final ClassPath other = (ClassPath) obj; 592 return Objects.equals(classPathString, other.classPathString); 593 } 594 595 /** 596 * @param name fully qualified file name, e.g. java/lang/String 597 * @return byte array for class 598 * @throws IOException if an I/O error occurs. 599 */ 600 public byte[] getBytes(final String name) throws IOException { 601 return getBytes(name, JavaClass.EXTENSION); 602 } 603 604 /** 605 * @param name fully qualified file name, e.g. java/lang/String 606 * @param suffix file name ends with suffix, e.g. .java 607 * @return byte array for file on class path 608 * @throws IOException if an I/O error occurs. 609 */ 610 public byte[] getBytes(final String name, final String suffix) throws IOException { 611 DataInputStream dis = null; 612 try (InputStream inputStream = getInputStream(name, suffix)) { 613 if (inputStream == null) { 614 throw new IOException("Couldn't find: " + name + suffix); 615 } 616 dis = new DataInputStream(inputStream); 617 final byte[] bytes = new byte[inputStream.available()]; 618 dis.readFully(bytes); 619 return bytes; 620 } finally { 621 if (dis != null) { 622 dis.close(); 623 } 624 } 625 } 626 627 /** 628 * @param name fully qualified class name, e.g. java.lang.String 629 * @return input stream for class 630 * @throws IOException if an I/O error occurs. 631 */ 632 public ClassFile getClassFile(final String name) throws IOException { 633 return getClassFile(name, JavaClass.EXTENSION); 634 } 635 636 /** 637 * @param name fully qualified file name, e.g. java/lang/String 638 * @param suffix file name ends with suff, e.g. .java 639 * @return class file for the Java class 640 * @throws IOException if an I/O error occurs. 641 */ 642 public ClassFile getClassFile(final String name, final String suffix) throws IOException { 643 ClassFile cf = null; 644 645 if (parent != null) { 646 cf = parent.getClassFileInternal(name, suffix); 647 } 648 649 if (cf == null) { 650 cf = getClassFileInternal(name, suffix); 651 } 652 653 if (cf != null) { 654 return cf; 655 } 656 657 throw new IOException("Couldn't find: " + name + suffix); 658 } 659 660 private ClassFile getClassFileInternal(final String name, final String suffix) { 661 for (final AbstractPathEntry path : paths) { 662 final ClassFile cf = path.getClassFile(name, suffix); 663 if (cf != null) { 664 return cf; 665 } 666 } 667 return null; 668 } 669 670 /** 671 * Gets an InputStream. 672 * <p> 673 * The caller is responsible for closing the InputStream. 674 * </p> 675 * @param name fully qualified class name, e.g. java.lang.String 676 * @return input stream for class 677 * @throws IOException if an I/O error occurs. 678 */ 679 public InputStream getInputStream(final String name) throws IOException { 680 return getInputStream(Utility.packageToPath(name), JavaClass.EXTENSION); 681 } 682 683 /** 684 * Gets an InputStream for a class or resource on the classpath. 685 * <p> 686 * The caller is responsible for closing the InputStream. 687 * </p> 688 * 689 * @param name fully qualified file name, e.g. java/lang/String 690 * @param suffix file name ends with suff, e.g. .java 691 * @return input stream for file on class path 692 * @throws IOException if an I/O error occurs. 693 */ 694 public InputStream getInputStream(final String name, final String suffix) throws IOException { 695 try { 696 final java.lang.ClassLoader classLoader = getClass().getClassLoader(); 697 @SuppressWarnings("resource") // closed by caller 698 final 699 InputStream inputStream = classLoader == null ? null : classLoader.getResourceAsStream(name + suffix); 700 if (inputStream != null) { 701 return inputStream; 702 } 703 } catch (final Exception ignored) { 704 // ignored 705 } 706 return getClassFile(name, suffix).getInputStream(); 707 } 708 709 /** 710 * @param name name of file to search for, e.g. java/lang/String.java 711 * @return full (canonical) path for file 712 * @throws IOException if an I/O error occurs. 713 */ 714 public String getPath(String name) throws IOException { 715 final int index = name.lastIndexOf('.'); 716 String suffix = ""; 717 if (index > 0) { 718 suffix = name.substring(index); 719 name = name.substring(0, index); 720 } 721 return getPath(name, suffix); 722 } 723 724 /** 725 * @param name name of file to search for, e.g. java/lang/String 726 * @param suffix file name suffix, e.g. .java 727 * @return full (canonical) path for file, if it exists 728 * @throws IOException if an I/O error occurs. 729 */ 730 public String getPath(final String name, final String suffix) throws IOException { 731 return getClassFile(name, suffix).getPath(); 732 } 733 734 /** 735 * @param name fully qualified resource name, e.g. java/lang/String.class 736 * @return URL supplying the resource, or null if no resource with that name. 737 * @since 6.0 738 */ 739 public URL getResource(final String name) { 740 for (final AbstractPathEntry path : paths) { 741 URL url; 742 if ((url = path.getResource(name)) != null) { 743 return url; 744 } 745 } 746 return null; 747 } 748 749 /** 750 * @param name fully qualified resource name, e.g. java/lang/String.class 751 * @return InputStream supplying the resource, or null if no resource with that name. 752 * @since 6.0 753 */ 754 public InputStream getResourceAsStream(final String name) { 755 for (final AbstractPathEntry path : paths) { 756 InputStream is; 757 if ((is = path.getResourceAsStream(name)) != null) { 758 return is; 759 } 760 } 761 return null; 762 } 763 764 /** 765 * @param name fully qualified resource name, e.g. java/lang/String.class 766 * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name. 767 * @since 6.0 768 */ 769 public Enumeration<URL> getResources(final String name) { 770 final Vector<URL> results = new Vector<>(); 771 for (final AbstractPathEntry path : paths) { 772 URL url; 773 if ((url = path.getResource(name)) != null) { 774 results.add(url); 775 } 776 } 777 return results.elements(); 778 } 779 780 @Override 781 public int hashCode() { 782 return classPathString.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 + classPathString; 792 } 793 return classPathString; 794 } 795}