/*
 * Copyright  2000-2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); 
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License. 
 *
 */
package org.apache.bcel.util;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Responsible for loading (class) files from the CLASSPATH. Inspired by
 * sun.tools.ClassPath.
 *
 * @version $Id: ClassPath.java 386056 2006-03-15 11:31:56Z tcurdt $
 * @author  <A HREF="mailto:m.dahm@gmx.de">M. Dahm</A>
 */
public class ClassPath implements Serializable {

    public static final ClassPath SYSTEM_CLASS_PATH = new ClassPath();
    private PathEntry[] paths;
    private String class_path;


    /**
     * Search for classes in given path.
     */
    public ClassPath(String class_path) {
        this.class_path = class_path;
        List vec = new ArrayList();
        for (StringTokenizer tok = new StringTokenizer(class_path, System
                .getProperty("path.separator")); tok.hasMoreTokens();) {
            String path = tok.nextToken();
            if (!path.equals("")) {
                File file = new File(path);
                try {
                    if (file.exists()) {
                        if (file.isDirectory()) {
                            vec.add(new Dir(path));
                        } else {
                            vec.add(new Zip(new ZipFile(file)));
                        }
                    }
                } catch (IOException e) {
                    System.err.println("CLASSPATH component " + file + ": " + e);
                }
            }
        }
        paths = new PathEntry[vec.size()];
        vec.toArray(paths);
    }


    /**
     * Search for classes in CLASSPATH.
     * @deprecated Use SYSTEM_CLASS_PATH constant
     */
    public ClassPath() {
        this(getClassPath());
    }


    /** @return used class path string
     */
    public String toString() {
        return class_path;
    }


    public int hashCode() {
        return class_path.hashCode();
    }


    public boolean equals( Object o ) {
        if (o instanceof ClassPath) {
            return class_path.equals(((ClassPath) o).class_path);
        }
        return false;
    }


    private static final void getPathComponents( String path, List list ) {
        if (path != null) {
            StringTokenizer tok = new StringTokenizer(path, File.pathSeparator);
            while (tok.hasMoreTokens()) {
                String name = tok.nextToken();
                File file = new File(name);
                if (file.exists()) {
                    list.add(name);
                }
            }
        }
    }


    /** Checks for class path components in the following properties:
     * "java.class.path", "sun.boot.class.path", "java.ext.dirs"
     *
     * @return class path as used by default by BCEL
     */
    public static final String getClassPath() {
        String class_path = System.getProperty("java.class.path");
        String boot_path = System.getProperty("sun.boot.class.path");
        String ext_path = System.getProperty("java.ext.dirs");
        List list = new ArrayList();
        getPathComponents(class_path, list);
        getPathComponents(boot_path, list);
        List dirs = new ArrayList();
        getPathComponents(ext_path, dirs);
        for (Iterator e = dirs.iterator(); e.hasNext();) {
            File ext_dir = new File((String) e.next());
            String[] extensions = ext_dir.list(new FilenameFilter() {

                public boolean accept( File dir, String name ) {
                    name = name.toLowerCase(Locale.ENGLISH);
                    return name.endsWith(".zip") || name.endsWith(".jar");
                }
            });
            if (extensions != null) {
                for (int i = 0; i < extensions.length; i++) {
                    list.add(ext_dir.getPath() + File.separatorChar + extensions[i]);
                }
            }
        }
        StringBuffer buf = new StringBuffer();
        for (Iterator e = list.iterator(); e.hasNext();) {
            buf.append((String) e.next());
            if (e.hasNext()) {
                buf.append(File.pathSeparatorChar);
            }
        }
        return buf.toString().intern();
    }


    /**
     * @param name fully qualified class name, e.g. java.lang.String
     * @return input stream for class
     */
    public InputStream getInputStream( String name ) throws IOException {
        return getInputStream(name.replace('.', '/'), ".class");
    }


    /**
     * Return stream for class or resource on CLASSPATH.
     *
     * @param name fully qualified file name, e.g. java/lang/String
     * @param suffix file name ends with suff, e.g. .java
     * @return input stream for file on class path
     */
    public InputStream getInputStream( String name, String suffix ) throws IOException {
        InputStream is = null;
        try {
            is = getClass().getClassLoader().getResourceAsStream(name + suffix);
        } catch (Exception e) {
        }
        if (is != null) {
            return is;
        }
        return getClassFile(name, suffix).getInputStream();
    }


    /**
     * @param name fully qualified file name, e.g. java/lang/String
     * @param suffix file name ends with suff, e.g. .java
     * @return class file for the java class
     */
    public ClassFile getClassFile( String name, String suffix ) throws IOException {
        for (int i = 0; i < paths.length; i++) {
            ClassFile cf;
            if ((cf = paths[i].getClassFile(name, suffix)) != null) {
                return cf;
            }
        }
        throw new IOException("Couldn't find: " + name + suffix);
    }


    /**
     * @param name fully qualified class name, e.g. java.lang.String
     * @return input stream for class
     */
    public ClassFile getClassFile( String name ) throws IOException {
        return getClassFile(name, ".class");
    }


    /**
     * @param name fully qualified file name, e.g. java/lang/String
     * @param suffix file name ends with suffix, e.g. .java
     * @return byte array for file on class path
     */
    public byte[] getBytes( String name, String suffix ) throws IOException {
        DataInputStream dis = null;
        try {
            InputStream is = getInputStream(name, suffix);
            if (is == null) {
                throw new IOException("Couldn't find: " + name + suffix);
            }
            dis = new DataInputStream(is);
            byte[] bytes = new byte[is.available()];
            dis.readFully(bytes);
            return bytes;
        } finally {
            if (dis != null) {
                dis.close();
            }
        }
    }


    /**
     * @return byte array for class
     */
    public byte[] getBytes( String name ) throws IOException {
        return getBytes(name, ".class");
    }


    /**
     * @param name name of file to search for, e.g. java/lang/String.java
     * @return full (canonical) path for file
     */
    public String getPath( String name ) throws IOException {
        int index = name.lastIndexOf('.');
        String suffix = "";
        if (index > 0) {
            suffix = name.substring(index);
            name = name.substring(0, index);
        }
        return getPath(name, suffix);
    }


    /**
     * @param name name of file to search for, e.g. java/lang/String
     * @param suffix file name suffix, e.g. .java
     * @return full (canonical) path for file, if it exists
     */
    public String getPath( String name, String suffix ) throws IOException {
        return getClassFile(name, suffix).getPath();
    }

    private static abstract class PathEntry implements Serializable {

        abstract ClassFile getClassFile( String name, String suffix ) throws IOException;
    }

    /** Contains information about file/ZIP entry of the Java class.
     */
    public interface ClassFile {

        /** @return input stream for class file.
         */
        public abstract InputStream getInputStream() throws IOException;


        /** @return canonical path to class file.
         */
        public abstract String getPath();


        /** @return base path of found class, i.e. class is contained relative
         * to that path, which may either denote a directory, or zip file
         */
        public abstract String getBase();


        /** @return modification time of class file.
         */
        public abstract long getTime();


        /** @return size of class file.
         */
        public abstract long getSize();
    }

    private static class Dir extends PathEntry {

        private String dir;


        Dir(String d) {
            dir = d;
        }


        ClassFile getClassFile( String name, String suffix ) throws IOException {
            final File file = new File(dir + File.separatorChar
                    + name.replace('.', File.separatorChar) + suffix);
            return file.exists() ? new ClassFile() {

                public InputStream getInputStream() throws IOException {
                    return new FileInputStream(file);
                }


                public String getPath() {
                    try {
                        return file.getCanonicalPath();
                    } catch (IOException e) {
                        return null;
                    }
                }


                public long getTime() {
                    return file.lastModified();
                }


                public long getSize() {
                    return file.length();
                }


                public String getBase() {
                    return dir;
                }
            } : null;
        }


        public String toString() {
            return dir;
        }
    }

    private static class Zip extends PathEntry {

        private ZipFile zip;


        Zip(ZipFile z) {
            zip = z;
        }


        ClassFile getClassFile( String name, String suffix ) throws IOException {
            final ZipEntry entry = zip.getEntry(name.replace('.', '/') + suffix);
            return (entry != null) ? new ClassFile() {

                public InputStream getInputStream() throws IOException {
                    return zip.getInputStream(entry);
                }


                public String getPath() {
                    return entry.toString();
                }


                public long getTime() {
                    return entry.getTime();
                }


                public long getSize() {
                    return entry.getSize();
                }


                public String getBase() {
                    return zip.getName();
                }
            } : null;
        }
    }
}
