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