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