/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.extension;

import java.io.IOException;
import java.util.List;
import java.util.Vector;

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.AbstractFileClassLoader;
import com.mucommander.commons.file.filter.AttributeFileFilter;
import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute;
import com.mucommander.commons.file.filter.ExtensionFilenameFilter;
import com.mucommander.commons.file.filter.OrFileFilter;

/**
 * Finds specific classes within a browsable file.
 * <p>
 * This class will explore the content of a browsable {@link com.mucommander.commons.file.AbstractFile} and match
 * all discovered classes to a {@link ClassFilter}.
 * </p>
 * <p>
 * In order for classes to be analyzed, they need to be loaded. This can be achieved in two ways:
 * <ul>
 *   <li>By using a custom class loader through {@link #find(AbstractFile,ClassFilter,ClassLoader)}.</li>
 *   <li>
 *     By using an {@link com.mucommander.commons.file.AbstractFileClassLoader}
 * </ul>
 * </p>
 * @author Nicolas Rinaudo
 */
public class ClassFinder {
    // - Instance fields -----------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /** ClassLoader used to load classes from explored files. */
    private ClassLoader  loader;
    /** Used to filter out files that are neither classes nor directories. */
    private OrFileFilter filter;
    /** Used to filter out unwanted classes. */
    private ClassFilter  classFilter;


    // - Initialization ------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /**
     * Creates a new instance of <code>ClassFinder</code>.
     */
    public ClassFinder() {
        filter = new OrFileFilter(
            new ExtensionFilenameFilter(".class"),
            new AttributeFileFilter(FileAttribute.DIRECTORY)
        );
    }



    // - File exploring ------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /**
     * Explores the specified file for classes that match {@link #classFilter}.
     * @param  currentPackage package we're currently exploring (with a trailing '.').
     * @param  currentFile    file we're currently exploring.
     * @return a vector containing all the classes that were found and matched <code>classFilter</code>.
     * @throws IOException    if an error occurs while exploring <code>currentFile</code>.
     */
    private List<Class<?>> find(String currentPackage, AbstractFile currentFile) throws IOException {
        AbstractFile[]   files;        // All subfolders or child class files of currentFile.
        Class<?>         currentClass; // Buffer for the current class.
        List<Class<?>>   result = new Vector<Class<?>>();
        
        // Analyses all subdirectories and class files.
        files = currentFile.ls(filter);
        for (AbstractFile file : files) {
            // Explores subdirectories recursively.
            if (file.isDirectory())
                result.addAll(find(currentPackage + file.getName() + '.', file));

                // Passes each class through the class filter.
                // Errors are treated as 'this class is not wanted'.
            else {
                try {
                    if (classFilter.accept(currentClass = Class.forName(currentPackage + file.getNameWithoutExtension(), false, loader)))
                        result.add(currentClass);
                }
                catch (Throwable e) {
                }
            }
        }
        return result;
    }



    // - Public code ---------------------------------------------------------------------
    // -----------------------------------------------------------------------------------
    /**
     * Explores the content of the specified file and looks for classes that match the specified class filter.
     * <p>
     * The <code>browsable</code> argument must be browsable as defined by {@link com.mucommander.commons.file.AbstractFile#isBrowsable()}.
     * If such is not the case, the returned vector will be empty.
     * </p>
     * @param  browsable   file in which to look for classes.
     * @param  classFilter how to decide which classes should be kept.
     * @param  classLoader used to load each class found in <code>browsable</code>.
     * @return             a vector containing all the classes that were found and matched <code>classFilter</code>.
     * @throws IOException if an error occurs while exploring <code>browsable</code>.
     * @see                #find(AbstractFile,ClassFilter)
     */
    public List<Class<?>> find(AbstractFile browsable, ClassFilter classFilter, ClassLoader classLoader) throws IOException {
        // Ignore non-browsable files.
        if(!browsable.isBrowsable())
            return new Vector<Class<?>>();

        // Initializes exploring.
        loader           = classLoader;
        this.classFilter = classFilter;

        // Looks for all matched classes in browsable.        
        return find("", browsable);
    }

    /**
     * Explores the content of the specified file and looks for classes that match the specified class filter.
     * <p>
     * This is a convenience method and is strictly equivalent to calling {@link #find(AbstractFile,ClassFilter,ClassLoader)}
     * with a class loader argument initialized with the following code:
     * <pre>
     * AbstractFileClassLoader loader;
     *
     * loader = new AbstractFileClassLoader();
     * loader.addFile(browsable);
     * </pre>
     * </p>
     * @param  browsable   file in which to look for classes.
     * @param  classFilter how to decide which classes should be kept.
     * @return             a vector containing all the classes that were found and matched <code>classFilter</code>.
     * @throws IOException if an error occurs while exploring <code>browsable</code>.
     */
    public List<Class<?>> find(AbstractFile browsable, ClassFilter classFilter) throws IOException {
        AbstractFileClassLoader classLoader; // Default class loader.

        // Initializes the default class loader.
        classLoader = new AbstractFileClassLoader();
        classLoader.addFile(browsable);

        // Explores browsable.
        return find(browsable, classFilter, classLoader);
    }
}
