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