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