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