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