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.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.util.ArrayList;
029import java.util.Enumeration;
030import java.util.List;
031import java.util.Locale;
032import java.util.StringTokenizer;
033import java.util.Vector;
034import java.util.zip.ZipEntry;
035import java.util.zip.ZipFile;
036
037/**
038 * Responsible for loading (class) files from the CLASSPATH. Inspired by
039 * sun.tools.ClassPath.
040 *
041 * @version $Id: ClassPath.java 1749603 2016-06-21 20:50:19Z ggregory $
042 */
043public class ClassPath {
044
045    public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath(getClassPath());
046
047    private static final FilenameFilter ARCHIVE_FILTER = new FilenameFilter() {
048
049        @Override
050        public boolean accept( final File dir, String name ) {
051            name = name.toLowerCase(Locale.ENGLISH);
052            return name.endsWith(".zip") || name.endsWith(".jar");
053        }
054    };
055
056    private final PathEntry[] paths;
057    private final String class_path;
058    private ClassPath parent;
059
060    public ClassPath(final ClassPath parent, final String class_path) {
061        this(class_path);
062        this.parent = parent;
063    }
064
065    /**
066     * Search for classes in given path.
067     * 
068     * @param class_path
069     */
070    public ClassPath(final String class_path) {
071        this.class_path = class_path;
072        final List<PathEntry> list = new ArrayList<>();
073        for (final StringTokenizer tok = new StringTokenizer(class_path, File.pathSeparator); tok.hasMoreTokens();) {
074            final String path = tok.nextToken();
075            if (!path.isEmpty()) {
076                final File file = new File(path);
077                try {
078                    if (file.exists()) {
079                        if (file.isDirectory()) {
080                            list.add(new Dir(path));
081                        } else {
082                            list.add(new Zip(new ZipFile(file)));
083                        }
084                    }
085                } catch (final IOException e) {
086                    if (path.endsWith(".zip") || path.endsWith(".jar")) {
087                        System.err.println("CLASSPATH component " + file + ": " + e);
088                    }
089                }
090            }
091        }
092        paths = new PathEntry[list.size()];
093        list.toArray(paths);
094    }
095
096    /**
097     * Search for classes in CLASSPATH.
098     * @deprecated Use SYSTEM_CLASS_PATH constant
099     */
100    @Deprecated
101    public ClassPath() {
102        this(getClassPath());
103    }
104
105    /** @return used class path string
106     */
107    @Override
108    public String toString() {
109        if (parent != null) {
110            return parent + File.pathSeparator + class_path;
111        }
112        return class_path;
113    }
114
115    @Override
116    public int hashCode() {
117        if (parent != null) {
118            return class_path.hashCode() + parent.hashCode();            
119        }
120        return class_path.hashCode();
121    }
122
123
124    @Override
125    public boolean equals( final Object o ) {
126        if (o instanceof ClassPath) {
127            final ClassPath cp = (ClassPath)o;
128            return class_path.equals(cp.toString());
129        }
130        return false;
131    }
132
133
134    private static void getPathComponents( final String path, final List<String> list ) {
135        if (path != null) {
136            final StringTokenizer tok = new StringTokenizer(path, File.pathSeparator);
137            while (tok.hasMoreTokens()) {
138                final String name = tok.nextToken();
139                final File file = new File(name);
140                if (file.exists()) {
141                    list.add(name);
142                }
143            }
144        }
145    }
146
147
148    /** Checks for class path components in the following properties:
149     * "java.class.path", "sun.boot.class.path", "java.ext.dirs"
150     *
151     * @return class path as used by default by BCEL
152     */
153    // @since 6.0 no longer final
154    public static String getClassPath() {
155        final String class_path = System.getProperty("java.class.path");
156        final String boot_path = System.getProperty("sun.boot.class.path");
157        final String ext_path = System.getProperty("java.ext.dirs");
158        final List<String> list = new ArrayList<>();
159        getPathComponents(class_path, list);
160        getPathComponents(boot_path, list);
161        final List<String> dirs = new ArrayList<>();
162        getPathComponents(ext_path, dirs);
163        for (final String d : dirs) {
164            final File ext_dir = new File(d);
165            final String[] extensions = ext_dir.list(ARCHIVE_FILTER);
166            if (extensions != null) {
167                for (final String extension : extensions) {
168                    list.add(ext_dir.getPath() + File.separatorChar + extension);
169                }
170            }
171        }
172        final StringBuilder buf = new StringBuilder();
173        String separator = "";
174        for (final String path : list) {
175            buf.append(separator);
176            separator = File.pathSeparator;
177            buf.append(path);
178        }
179        return buf.toString().intern();
180    }
181
182
183    /**
184     * @param name fully qualified class name, e.g. java.lang.String
185     * @return input stream for class
186     */
187    public InputStream getInputStream( final String name ) throws IOException {
188        return getInputStream(name.replace('.', '/'), ".class");
189    }
190
191
192    /**
193     * Return stream for class or resource on CLASSPATH.
194     *
195     * @param name fully qualified file name, e.g. java/lang/String
196     * @param suffix file name ends with suff, e.g. .java
197     * @return input stream for file on class path
198     */
199    public InputStream getInputStream( final String name, final String suffix ) throws IOException {
200        InputStream is = null;
201        try {
202            is = getClass().getClassLoader().getResourceAsStream(name + suffix); // may return null
203        } catch (final Exception e) {
204            // ignored
205        }
206        if (is != null) {
207            return is;
208        }
209        return getClassFile(name, suffix).getInputStream();
210    }
211
212    /**
213     * @param name fully qualified resource name, e.g. java/lang/String.class
214     * @return InputStream supplying the resource, or null if no resource with that name.
215     * @since 6.0
216     */
217    public InputStream getResourceAsStream(final String name) {
218        for (final PathEntry path : paths) {
219            InputStream is;
220            if ((is = path.getResourceAsStream(name)) != null) {
221                return is;
222            }
223        }
224        return null;
225    }
226
227    /**
228     * @param name fully qualified resource name, e.g. java/lang/String.class
229     * @return URL supplying the resource, or null if no resource with that name.
230     * @since 6.0
231     */
232    public URL getResource(final String name) {
233        for (final PathEntry path : paths) {
234            URL url;
235            if ((url = path.getResource(name)) != null) {
236                return url;
237            }
238        }
239        return null;
240    }
241
242    /**
243     * @param name fully qualified resource name, e.g. java/lang/String.class
244     * @return An Enumeration of URLs supplying the resource, or an
245     * empty Enumeration if no resource with that name.
246     * @since 6.0
247     */
248    public Enumeration<URL> getResources(final String name) {
249        final Vector<URL> results = new Vector<>();
250        for (final PathEntry path : paths) {
251            URL url;
252            if ((url = path.getResource(name)) != null) {
253                results.add(url);
254            }
255        }
256        return results.elements();
257    }
258
259    /**
260     * @param name fully qualified file name, e.g. java/lang/String
261     * @param suffix file name ends with suff, e.g. .java
262     * @return class file for the java class
263     */
264    public ClassFile getClassFile( final String name, final String suffix ) throws IOException {
265        ClassFile cf = null;
266
267        if (parent != null) {
268            cf = parent.getClassFileInternal(name, suffix);
269        }
270
271        if (cf == null) {
272            cf = getClassFileInternal(name, suffix);
273        }
274
275        if (cf != null) {
276            return cf;
277        }
278
279        throw new IOException("Couldn't find: " + name + suffix);
280    }
281
282    private ClassFile getClassFileInternal(final String name, final String suffix) throws IOException {
283
284      for (final PathEntry path : paths) {
285          final ClassFile cf = path.getClassFile(name, suffix);
286
287          if(cf != null) {
288              return cf;
289          }
290      }
291
292      return null;
293   }
294
295
296    /**
297     * @param name fully qualified class name, e.g. java.lang.String
298     * @return input stream for class
299     */
300    public ClassFile getClassFile( final String name ) throws IOException {
301        return getClassFile(name, ".class");
302    }
303
304
305    /**
306     * @param name fully qualified file name, e.g. java/lang/String
307     * @param suffix file name ends with suffix, e.g. .java
308     * @return byte array for file on class path
309     */
310    public byte[] getBytes(final String name, final String suffix) throws IOException {
311        DataInputStream dis = null;
312        try (InputStream is = getInputStream(name, suffix)) {
313            if (is == null) {
314                throw new IOException("Couldn't find: " + name + suffix);
315            }
316            dis = new DataInputStream(is);
317            final byte[] bytes = new byte[is.available()];
318            dis.readFully(bytes);
319            return bytes;
320        } finally {
321            if (dis != null) {
322                dis.close();
323            }
324        }
325    }
326
327
328    /**
329     * @return byte array for class
330     */
331    public byte[] getBytes( final String name ) throws IOException {
332        return getBytes(name, ".class");
333    }
334
335
336    /**
337     * @param name name of file to search for, e.g. java/lang/String.java
338     * @return full (canonical) path for file
339     */
340    public String getPath( String name ) throws IOException {
341        final int index = name.lastIndexOf('.');
342        String suffix = "";
343        if (index > 0) {
344            suffix = name.substring(index);
345            name = name.substring(0, index);
346        }
347        return getPath(name, suffix);
348    }
349
350
351    /**
352     * @param name name of file to search for, e.g. java/lang/String
353     * @param suffix file name suffix, e.g. .java
354     * @return full (canonical) path for file, if it exists
355     */
356    public String getPath( final String name, final String suffix ) throws IOException {
357        return getClassFile(name, suffix).getPath();
358    }
359
360    private abstract static class PathEntry {
361
362        abstract ClassFile getClassFile( String name, String suffix ) throws IOException;
363        abstract URL getResource(String name);
364        abstract InputStream getResourceAsStream(String name);
365    }
366
367    /** Contains information about file/ZIP entry of the Java class.
368     */
369    public interface ClassFile {
370
371        /** @return input stream for class file.
372         */
373        InputStream getInputStream() throws IOException;
374
375
376        /** @return canonical path to class file.
377         */
378        String getPath();
379
380
381        /** @return base path of found class, i.e. class is contained relative
382         * to that path, which may either denote a directory, or zip file
383         */
384        String getBase();
385
386
387        /** @return modification time of class file.
388         */
389        long getTime();
390
391
392        /** @return size of class file.
393         */
394        long getSize();
395    }
396
397    private static class Dir extends PathEntry {
398
399        private final String dir;
400
401
402        Dir(final String d) {
403            dir = d;
404        }
405
406        @Override
407        URL getResource(final String name) {
408            // Resource specification uses '/' whatever the platform
409            final File file = new File(dir + File.separatorChar + name.replace('/', File.separatorChar));
410            try {
411                return file.exists() ? file.toURI().toURL() : null;
412            } catch (final MalformedURLException e) {
413               return null;
414            }
415        }
416
417        @Override
418        InputStream getResourceAsStream(final String name) {
419            // Resource specification uses '/' whatever the platform
420            final File file = new File(dir + File.separatorChar + name.replace('/', File.separatorChar));
421            try {
422               return file.exists() ? new FileInputStream(file) : null;
423            } catch (final IOException e) {
424               return null;
425            }
426        }
427
428        @Override
429        ClassFile getClassFile( final String name, final String suffix ) throws IOException {
430            final File file = new File(dir + File.separatorChar
431                    + name.replace('.', File.separatorChar) + suffix);
432            return file.exists() ? new ClassFile() {
433
434                @Override
435                public InputStream getInputStream() throws IOException {
436                    return new FileInputStream(file);
437                }
438
439
440                @Override
441                public String getPath() {
442                    try {
443                        return file.getCanonicalPath();
444                    } catch (final IOException e) {
445                        return null;
446                    }
447                }
448
449
450                @Override
451                public long getTime() {
452                    return file.lastModified();
453                }
454
455
456                @Override
457                public long getSize() {
458                    return file.length();
459                }
460
461
462                @Override
463                public String getBase() {
464                    return dir;
465                }
466            } : null;
467        }
468
469
470        @Override
471        public String toString() {
472            return dir;
473        }
474    }
475
476    private static class Zip extends PathEntry {
477
478        private final ZipFile zip;
479
480
481        Zip(final ZipFile z) {
482            zip = z;
483        }
484
485        @Override
486        URL getResource(final String name) {
487            final ZipEntry entry = zip.getEntry(name);
488            try {
489                return (entry != null) ? new URL("jar:file:" + zip.getName() + "!/" + name) : null;
490            } catch (final MalformedURLException e) {
491                return null;
492           }
493        }
494
495        @Override
496        InputStream getResourceAsStream(final String name) {
497            final ZipEntry entry = zip.getEntry(name);
498            try {
499                return (entry != null) ? zip.getInputStream(entry) : null;
500            } catch (final IOException e) {
501                return null;
502            }
503        }
504
505        @Override
506        ClassFile getClassFile( final String name, final String suffix ) throws IOException {
507            final ZipEntry entry = zip.getEntry(name.replace('.', '/') + suffix);
508
509            if (entry == null) {
510                return null;
511            }
512
513            return new ClassFile() {
514
515                @Override
516                public InputStream getInputStream() throws IOException {
517                    return zip.getInputStream(entry);
518                }
519
520
521                @Override
522                public String getPath() {
523                    return entry.toString();
524                }
525
526
527                @Override
528                public long getTime() {
529                    return entry.getTime();
530                }
531
532
533                @Override
534                public long getSize() {
535                    return entry.getSize();
536                }
537
538
539                @Override
540                public String getBase() {
541                    return zip.getName();
542                }
543            };
544        }
545    }
546}