ClassPath.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  *  Unless required by applicable law or agreed to in writing, software
  12.  *  distributed under the License is distributed on an "AS IS" BASIS,
  13.  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  *  See the License for the specific language governing permissions and
  15.  *  limitations under the License.
  16.  */
  17. package org.apache.bcel.util;

  18. import java.io.Closeable;
  19. import java.io.DataInputStream;
  20. import java.io.File;
  21. import java.io.FileInputStream;
  22. import java.io.FilenameFilter;
  23. import java.io.IOException;
  24. import java.io.InputStream;
  25. import java.net.MalformedURLException;
  26. import java.net.URL;
  27. import java.nio.file.Files;
  28. import java.nio.file.Path;
  29. import java.nio.file.Paths;
  30. import java.util.ArrayList;
  31. import java.util.Arrays;
  32. import java.util.Enumeration;
  33. import java.util.List;
  34. import java.util.Locale;
  35. import java.util.Objects;
  36. import java.util.StringTokenizer;
  37. import java.util.Vector;
  38. import java.util.stream.Collectors;
  39. import java.util.zip.ZipEntry;
  40. import java.util.zip.ZipFile;

  41. import org.apache.bcel.classfile.JavaClass;
  42. import org.apache.bcel.classfile.Utility;
  43. import org.apache.commons.lang3.SystemProperties;

  44. /**
  45.  * Loads class files from the CLASSPATH. Inspired by sun.tools.ClassPath.
  46.  */
  47. public class ClassPath implements Closeable {

  48.     private abstract static class AbstractPathEntry implements Closeable {

  49.         abstract ClassFile getClassFile(String name, String suffix);

  50.         abstract URL getResource(String name);

  51.         abstract InputStream getResourceAsStream(String name);
  52.     }

  53.     private abstract static class AbstractZip extends AbstractPathEntry {

  54.         private final ZipFile zipFile;

  55.         AbstractZip(final ZipFile zipFile) {
  56.             this.zipFile = Objects.requireNonNull(zipFile, "zipFile");
  57.         }

  58.         @Override
  59.         public void close() throws IOException {
  60.             if (zipFile != null) {
  61.                 zipFile.close();
  62.             }

  63.         }

  64.         @Override
  65.         ClassFile getClassFile(final String name, final String suffix) {
  66.             final ZipEntry entry = zipFile.getEntry(toEntryName(name, suffix));

  67.             if (entry == null) {
  68.                 return null;
  69.             }

  70.             return new ClassFile() {

  71.                 @Override
  72.                 public String getBase() {
  73.                     return zipFile.getName();
  74.                 }

  75.                 @Override
  76.                 public InputStream getInputStream() throws IOException {
  77.                     return zipFile.getInputStream(entry);
  78.                 }

  79.                 @Override
  80.                 public String getPath() {
  81.                     return entry.toString();
  82.                 }

  83.                 @Override
  84.                 public long getSize() {
  85.                     return entry.getSize();
  86.                 }

  87.                 @Override
  88.                 public long getTime() {
  89.                     return entry.getTime();
  90.                 }
  91.             };
  92.         }

  93.         @Override
  94.         URL getResource(final String name) {
  95.             final ZipEntry entry = zipFile.getEntry(name);
  96.             try {
  97.                 return entry != null ? new URL("jar:file:" + zipFile.getName() + "!/" + name) : null;
  98.             } catch (final MalformedURLException e) {
  99.                 return null;
  100.             }
  101.         }

  102.         @Override
  103.         InputStream getResourceAsStream(final String name) {
  104.             final ZipEntry entry = zipFile.getEntry(name);
  105.             try {
  106.                 return entry != null ? zipFile.getInputStream(entry) : null;
  107.             } catch (final IOException e) {
  108.                 return null;
  109.             }
  110.         }

  111.         protected abstract String toEntryName(final String name, final String suffix);

  112.         @Override
  113.         public String toString() {
  114.             return zipFile.getName();
  115.         }

  116.     }

  117.     /**
  118.      * Contains information about file/ZIP entry of the Java class.
  119.      */
  120.     public interface ClassFile {

  121.         /**
  122.          * @return base path of found class, i.e. class is contained relative to that path, which may either denote a directory,
  123.          *         or ZIP file
  124.          */
  125.         String getBase();

  126.         /**
  127.          * @return input stream for class file.
  128.          * @throws IOException if an I/O error occurs.
  129.          */
  130.         InputStream getInputStream() throws IOException;

  131.         /**
  132.          * @return canonical path to class file.
  133.          */
  134.         String getPath();

  135.         /**
  136.          * @return size of class file.
  137.          */
  138.         long getSize();

  139.         /**
  140.          * @return modification time of class file.
  141.          */
  142.         long getTime();
  143.     }

  144.     private static final class Dir extends AbstractPathEntry {

  145.         private final String dir;

  146.         Dir(final String d) {
  147.             dir = d;
  148.         }

  149.         @Override
  150.         public void close() throws IOException {
  151.             // Nothing to do

  152.         }

  153.         @Override
  154.         ClassFile getClassFile(final String name, final String suffix) {
  155.             final File file = new File(dir + File.separatorChar + name.replace('.', File.separatorChar) + suffix);
  156.             return file.exists() ? new ClassFile() {

  157.                 @Override
  158.                 public String getBase() {
  159.                     return dir;
  160.                 }

  161.                 @Override
  162.                 public InputStream getInputStream() throws IOException {
  163.                     return new FileInputStream(file);
  164.                 }

  165.                 @Override
  166.                 public String getPath() {
  167.                     try {
  168.                         return file.getCanonicalPath();
  169.                     } catch (final IOException e) {
  170.                         return null;
  171.                     }
  172.                 }

  173.                 @Override
  174.                 public long getSize() {
  175.                     return file.length();
  176.                 }

  177.                 @Override
  178.                 public long getTime() {
  179.                     return file.lastModified();
  180.                 }
  181.             } : null;
  182.         }

  183.         @Override
  184.         URL getResource(final String name) {
  185.             // Resource specification uses '/' whatever the platform
  186.             final File file = toFile(name);
  187.             try {
  188.                 return file.exists() ? file.toURI().toURL() : null;
  189.             } catch (final MalformedURLException e) {
  190.                 return null;
  191.             }
  192.         }

  193.         @Override
  194.         InputStream getResourceAsStream(final String name) {
  195.             // Resource specification uses '/' whatever the platform
  196.             final File file = toFile(name);
  197.             try {
  198.                 return file.exists() ? new FileInputStream(file) : null;
  199.             } catch (final IOException e) {
  200.                 return null;
  201.             }
  202.         }

  203.         private File toFile(final String name) {
  204.             return new File(dir + File.separatorChar + name.replace('/', File.separatorChar));
  205.         }

  206.         @Override
  207.         public String toString() {
  208.             return dir;
  209.         }
  210.     }

  211.     private static final class Jar extends AbstractZip {

  212.         Jar(final ZipFile zip) {
  213.             super(zip);
  214.         }

  215.         @Override
  216.         protected String toEntryName(final String name, final String suffix) {
  217.             return Utility.packageToPath(name) + suffix;
  218.         }

  219.     }

  220.     private static final class JrtModule extends AbstractPathEntry {

  221.         private final Path modulePath;

  222.         public JrtModule(final Path modulePath) {
  223.             this.modulePath = Objects.requireNonNull(modulePath, "modulePath");
  224.         }

  225.         @Override
  226.         public void close() throws IOException {
  227.             // Nothing to do.

  228.         }

  229.         @Override
  230.         ClassFile getClassFile(final String name, final String suffix) {
  231.             final Path resolved = modulePath.resolve(Utility.packageToPath(name) + suffix);
  232.             if (Files.exists(resolved)) {
  233.                 return new ClassFile() {

  234.                     @Override
  235.                     public String getBase() {
  236.                         return Objects.toString(resolved.getFileName(), null);
  237.                     }

  238.                     @Override
  239.                     public InputStream getInputStream() throws IOException {
  240.                         return Files.newInputStream(resolved);
  241.                     }

  242.                     @Override
  243.                     public String getPath() {
  244.                         return resolved.toString();
  245.                     }

  246.                     @Override
  247.                     public long getSize() {
  248.                         try {
  249.                             return Files.size(resolved);
  250.                         } catch (final IOException e) {
  251.                             return 0;
  252.                         }
  253.                     }

  254.                     @Override
  255.                     public long getTime() {
  256.                         try {
  257.                             return Files.getLastModifiedTime(resolved).toMillis();
  258.                         } catch (final IOException e) {
  259.                             return 0;
  260.                         }
  261.                     }
  262.                 };
  263.             }
  264.             return null;
  265.         }

  266.         @Override
  267.         URL getResource(final String name) {
  268.             final Path resovled = modulePath.resolve(name);
  269.             try {
  270.                 return Files.exists(resovled) ? new URL("jrt:" + modulePath + "/" + name) : null;
  271.             } catch (final MalformedURLException e) {
  272.                 return null;
  273.             }
  274.         }

  275.         @Override
  276.         InputStream getResourceAsStream(final String name) {
  277.             try {
  278.                 return Files.newInputStream(modulePath.resolve(name));
  279.             } catch (final IOException e) {
  280.                 return null;
  281.             }
  282.         }

  283.         @Override
  284.         public String toString() {
  285.             return modulePath.toString();
  286.         }

  287.     }

  288.     private static final class JrtModules extends AbstractPathEntry {

  289.         private final ModularRuntimeImage modularRuntimeImage;
  290.         private final JrtModule[] modules;

  291.         public JrtModules(final String path) throws IOException {
  292.             this.modularRuntimeImage = new ModularRuntimeImage();
  293.             this.modules = modularRuntimeImage.list(path).stream().map(JrtModule::new).toArray(JrtModule[]::new);
  294.         }

  295.         @Override
  296.         public void close() throws IOException {
  297.             if (modules != null) {
  298.                 // don't use a for each loop to avoid creating an iterator for the GC to collect.
  299.                 for (final JrtModule module : modules) {
  300.                     module.close();
  301.                 }
  302.             }
  303.             if (modularRuntimeImage != null) {
  304.                 modularRuntimeImage.close();
  305.             }
  306.         }

  307.         @Override
  308.         ClassFile getClassFile(final String name, final String suffix) {
  309.             // don't use a for each loop to avoid creating an iterator for the GC to collect.
  310.             for (final JrtModule module : modules) {
  311.                 final ClassFile classFile = module.getClassFile(name, suffix);
  312.                 if (classFile != null) {
  313.                     return classFile;
  314.                 }
  315.             }
  316.             return null;
  317.         }

  318.         @Override
  319.         URL getResource(final String name) {
  320.             // don't use a for each loop to avoid creating an iterator for the GC to collect.
  321.             for (final JrtModule module : modules) {
  322.                 final URL url = module.getResource(name);
  323.                 if (url != null) {
  324.                     return url;
  325.                 }
  326.             }
  327.             return null;
  328.         }

  329.         @Override
  330.         InputStream getResourceAsStream(final String name) {
  331.             // don't use a for each loop to avoid creating an iterator for the GC to collect.
  332.             for (final JrtModule module : modules) {
  333.                 final InputStream inputStream = module.getResourceAsStream(name);
  334.                 if (inputStream != null) {
  335.                     return inputStream;
  336.                 }
  337.             }
  338.             return null;
  339.         }

  340.         @Override
  341.         public String toString() {
  342.             return Arrays.toString(modules);
  343.         }

  344.     }

  345.     private static final class Module extends AbstractZip {

  346.         Module(final ZipFile zip) {
  347.             super(zip);
  348.         }

  349.         @Override
  350.         protected String toEntryName(final String name, final String suffix) {
  351.             return "classes/" + Utility.packageToPath(name) + suffix;
  352.         }

  353.     }

  354.     private static final FilenameFilter ARCHIVE_FILTER = (dir, name) -> {
  355.         name = name.toLowerCase(Locale.ENGLISH);
  356.         return name.endsWith(".zip") || name.endsWith(".jar");
  357.     };

  358.     private static final FilenameFilter MODULES_FILTER = (dir, name) -> {
  359.         name = name.toLowerCase(Locale.ENGLISH);
  360.         return name.endsWith(org.apache.bcel.classfile.Module.EXTENSION);
  361.     };

  362.     public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath(getClassPath());

  363.     private static void addJdkModules(final String javaHome, final List<String> list) {
  364.         String modulesPath = System.getProperty("java.modules.path");
  365.         if (modulesPath == null || modulesPath.trim().isEmpty()) {
  366.             // Default to looking in JAVA_HOME/jmods
  367.             modulesPath = javaHome + File.separator + "jmods";
  368.         }
  369.         final File modulesDir = new File(modulesPath);
  370.         if (modulesDir.exists()) {
  371.             final String[] modules = modulesDir.list(MODULES_FILTER);
  372.             if (modules != null) {
  373.                 for (final String module : modules) {
  374.                     list.add(modulesDir.getPath() + File.separatorChar + module);
  375.                 }
  376.             }
  377.         }
  378.     }

  379.     /**
  380.      * Checks for class path components in the following properties: "java.class.path", "sun.boot.class.path",
  381.      * "java.ext.dirs"
  382.      *
  383.      * @return class path as used by default by BCEL
  384.      */
  385.     // @since 6.0 no longer final
  386.     public static String getClassPath() {
  387.         final String classPathProp = SystemProperties.getJavaClassPath();
  388.         final String bootClassPathProp = System.getProperty("sun.boot.class.path");
  389.         final String extDirs = SystemProperties.getJavaExtDirs();
  390.         // System.out.println("java.version = " + System.getProperty("java.version"));
  391.         // System.out.println("java.class.path = " + classPathProp);
  392.         // System.out.println("sun.boot.class.path=" + bootClassPathProp);
  393.         // System.out.println("java.ext.dirs=" + extDirs);
  394.         final String javaHome = SystemProperties.getJavaHome();
  395.         final List<String> list = new ArrayList<>();

  396.         // Starting in JRE 9, .class files are in the modules directory. Add them to the path.
  397.         final Path modulesPath = Paths.get(javaHome).resolve("lib/modules");
  398.         if (Files.exists(modulesPath) && Files.isRegularFile(modulesPath)) {
  399.             list.add(modulesPath.toAbsolutePath().toString());
  400.         }
  401.         // Starting in JDK 9, .class files are in the jmods directory. Add them to the path.
  402.         addJdkModules(javaHome, list);

  403.         getPathComponents(classPathProp, list);
  404.         getPathComponents(bootClassPathProp, list);
  405.         final List<String> dirs = new ArrayList<>();
  406.         getPathComponents(extDirs, dirs);
  407.         for (final String d : dirs) {
  408.             final File extDir = new File(d);
  409.             final String[] extensions = extDir.list(ARCHIVE_FILTER);
  410.             if (extensions != null) {
  411.                 for (final String extension : extensions) {
  412.                     list.add(extDir.getPath() + File.separatorChar + extension);
  413.                 }
  414.             }
  415.         }

  416.         return list.stream().collect(Collectors.joining(File.pathSeparator));
  417.     }

  418.     private static void getPathComponents(final String path, final List<String> list) {
  419.         if (path != null) {
  420.             final StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator);
  421.             while (tokenizer.hasMoreTokens()) {
  422.                 final String name = tokenizer.nextToken();
  423.                 final File file = new File(name);
  424.                 if (file.exists()) {
  425.                     list.add(name);
  426.                 }
  427.             }
  428.         }
  429.     }

  430.     private final String classPathString;

  431.     private final ClassPath parent;

  432.     private final List<AbstractPathEntry> paths;

  433.     /**
  434.      * Search for classes in CLASSPATH.
  435.      *
  436.      * @deprecated Use SYSTEM_CLASS_PATH constant
  437.      */
  438.     @Deprecated
  439.     public ClassPath() {
  440.         this(getClassPath());
  441.     }

  442.     @SuppressWarnings("resource")
  443.     public ClassPath(final ClassPath parent, final String classPathString) {
  444.         this.parent = parent;
  445.         this.classPathString = Objects.requireNonNull(classPathString, "classPathString");
  446.         this.paths = new ArrayList<>();
  447.         for (final StringTokenizer tokenizer = new StringTokenizer(classPathString, File.pathSeparator); tokenizer.hasMoreTokens();) {
  448.             final String path = tokenizer.nextToken();
  449.             if (!path.isEmpty()) {
  450.                 final File file = new File(path);
  451.                 try {
  452.                     if (file.exists()) {
  453.                         if (file.isDirectory()) {
  454.                             paths.add(new Dir(path));
  455.                         } else if (path.endsWith(org.apache.bcel.classfile.Module.EXTENSION)) {
  456.                             paths.add(new Module(new ZipFile(file)));
  457.                         } else if (path.endsWith(ModularRuntimeImage.MODULES_PATH)) {
  458.                             paths.add(new JrtModules(ModularRuntimeImage.MODULES_PATH));
  459.                         } else {
  460.                             paths.add(new Jar(new ZipFile(file)));
  461.                         }
  462.                     }
  463.                 } catch (final IOException e) {
  464.                     if (path.endsWith(".zip") || path.endsWith(".jar")) {
  465.                         System.err.println("CLASSPATH component " + file + ": " + e);
  466.                     }
  467.                 }
  468.             }
  469.         }
  470.     }

  471.     /**
  472.      * Search for classes in given path.
  473.      *
  474.      * @param classPath
  475.      */
  476.     public ClassPath(final String classPath) {
  477.         this(null, classPath);
  478.     }

  479.     @Override
  480.     public void close() throws IOException {
  481.         for (final AbstractPathEntry path : paths) {
  482.             path.close();
  483.         }
  484.     }

  485.     @Override
  486.     public boolean equals(final Object obj) {
  487.         if (this == obj) {
  488.             return true;
  489.         }
  490.         if (obj == null) {
  491.             return false;
  492.         }
  493.         if (getClass() != obj.getClass()) {
  494.             return false;
  495.         }
  496.         final ClassPath other = (ClassPath) obj;
  497.         return Objects.equals(classPathString, other.classPathString);
  498.     }

  499.     /**
  500.      * @param name fully qualified file name, e.g. java/lang/String
  501.      * @return byte array for class
  502.      * @throws IOException if an I/O error occurs.
  503.      */
  504.     public byte[] getBytes(final String name) throws IOException {
  505.         return getBytes(name, JavaClass.EXTENSION);
  506.     }

  507.     /**
  508.      * @param name fully qualified file name, e.g. java/lang/String
  509.      * @param suffix file name ends with suffix, e.g. .java
  510.      * @return byte array for file on class path
  511.      * @throws IOException if an I/O error occurs.
  512.      */
  513.     public byte[] getBytes(final String name, final String suffix) throws IOException {
  514.         DataInputStream dis = null;
  515.         try (InputStream inputStream = getInputStream(name, suffix)) {
  516.             if (inputStream == null) {
  517.                 throw new IOException("Couldn't find: " + name + suffix);
  518.             }
  519.             dis = new DataInputStream(inputStream);
  520.             final byte[] bytes = new byte[inputStream.available()];
  521.             dis.readFully(bytes);
  522.             return bytes;
  523.         } finally {
  524.             if (dis != null) {
  525.                 dis.close();
  526.             }
  527.         }
  528.     }

  529.     /**
  530.      * @param name fully qualified class name, e.g. java.lang.String
  531.      * @return input stream for class
  532.      * @throws IOException if an I/O error occurs.
  533.      */
  534.     public ClassFile getClassFile(final String name) throws IOException {
  535.         return getClassFile(name, JavaClass.EXTENSION);
  536.     }

  537.     /**
  538.      * @param name fully qualified file name, e.g. java/lang/String
  539.      * @param suffix file name ends with suff, e.g. .java
  540.      * @return class file for the Java class
  541.      * @throws IOException if an I/O error occurs.
  542.      */
  543.     public ClassFile getClassFile(final String name, final String suffix) throws IOException {
  544.         ClassFile cf = null;

  545.         if (parent != null) {
  546.             cf = parent.getClassFileInternal(name, suffix);
  547.         }

  548.         if (cf == null) {
  549.             cf = getClassFileInternal(name, suffix);
  550.         }

  551.         if (cf != null) {
  552.             return cf;
  553.         }

  554.         throw new IOException("Couldn't find: " + name + suffix);
  555.     }

  556.     private ClassFile getClassFileInternal(final String name, final String suffix) {
  557.         for (final AbstractPathEntry path : paths) {
  558.             final ClassFile cf = path.getClassFile(name, suffix);
  559.             if (cf != null) {
  560.                 return cf;
  561.             }
  562.         }
  563.         return null;
  564.     }

  565.     /**
  566.      * Gets an InputStream.
  567.      * <p>
  568.      * The caller is responsible for closing the InputStream.
  569.      * </p>
  570.      * @param name fully qualified class name, e.g. java.lang.String
  571.      * @return input stream for class
  572.      * @throws IOException if an I/O error occurs.
  573.      */
  574.     public InputStream getInputStream(final String name) throws IOException {
  575.         return getInputStream(Utility.packageToPath(name), JavaClass.EXTENSION);
  576.     }

  577.     /**
  578.      * Gets an InputStream for a class or resource on the classpath.
  579.      * <p>
  580.      * The caller is responsible for closing the InputStream.
  581.      * </p>
  582.      *
  583.      * @param name   fully qualified file name, e.g. java/lang/String
  584.      * @param suffix file name ends with suff, e.g. .java
  585.      * @return input stream for file on class path
  586.      * @throws IOException if an I/O error occurs.
  587.      */
  588.     public InputStream getInputStream(final String name, final String suffix) throws IOException {
  589.         try {
  590.             final java.lang.ClassLoader classLoader = getClass().getClassLoader();
  591.             @SuppressWarnings("resource") // closed by caller
  592.             final
  593.             InputStream inputStream = classLoader == null ? null : classLoader.getResourceAsStream(name + suffix);
  594.             if (inputStream != null) {
  595.                 return inputStream;
  596.             }
  597.         } catch (final Exception ignored) {
  598.             // ignored
  599.         }
  600.         return getClassFile(name, suffix).getInputStream();
  601.     }

  602.     /**
  603.      * @param name name of file to search for, e.g. java/lang/String.java
  604.      * @return full (canonical) path for file
  605.      * @throws IOException if an I/O error occurs.
  606.      */
  607.     public String getPath(String name) throws IOException {
  608.         final int index = name.lastIndexOf('.');
  609.         String suffix = "";
  610.         if (index > 0) {
  611.             suffix = name.substring(index);
  612.             name = name.substring(0, index);
  613.         }
  614.         return getPath(name, suffix);
  615.     }

  616.     /**
  617.      * @param name name of file to search for, e.g. java/lang/String
  618.      * @param suffix file name suffix, e.g. .java
  619.      * @return full (canonical) path for file, if it exists
  620.      * @throws IOException if an I/O error occurs.
  621.      */
  622.     public String getPath(final String name, final String suffix) throws IOException {
  623.         return getClassFile(name, suffix).getPath();
  624.     }

  625.     /**
  626.      * @param name fully qualified resource name, e.g. java/lang/String.class
  627.      * @return URL supplying the resource, or null if no resource with that name.
  628.      * @since 6.0
  629.      */
  630.     public URL getResource(final String name) {
  631.         for (final AbstractPathEntry path : paths) {
  632.             URL url;
  633.             if ((url = path.getResource(name)) != null) {
  634.                 return url;
  635.             }
  636.         }
  637.         return null;
  638.     }

  639.     /**
  640.      * @param name fully qualified resource name, e.g. java/lang/String.class
  641.      * @return InputStream supplying the resource, or null if no resource with that name.
  642.      * @since 6.0
  643.      */
  644.     public InputStream getResourceAsStream(final String name) {
  645.         for (final AbstractPathEntry path : paths) {
  646.             InputStream is;
  647.             if ((is = path.getResourceAsStream(name)) != null) {
  648.                 return is;
  649.             }
  650.         }
  651.         return null;
  652.     }

  653.     /**
  654.      * @param name fully qualified resource name, e.g. java/lang/String.class
  655.      * @return An Enumeration of URLs supplying the resource, or an empty Enumeration if no resource with that name.
  656.      * @since 6.0
  657.      */
  658.     public Enumeration<URL> getResources(final String name) {
  659.         final Vector<URL> results = new Vector<>();
  660.         for (final AbstractPathEntry path : paths) {
  661.             URL url;
  662.             if ((url = path.getResource(name)) != null) {
  663.                 results.add(url);
  664.             }
  665.         }
  666.         return results.elements();
  667.     }

  668.     @Override
  669.     public int hashCode() {
  670.         return classPathString.hashCode();
  671.     }

  672.     /**
  673.      * @return used class path string
  674.      */
  675.     @Override
  676.     public String toString() {
  677.         if (parent != null) {
  678.             return parent + File.pathSeparator + classPathString;
  679.         }
  680.         return classPathString;
  681.     }
  682. }