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 final 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 final 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 final 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 final 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 final 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}