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