/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program 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 Lesser General Public License for more details.
 *
 * Copyright (c) 2007 - 2009 Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.base.util;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The class-query tool loads classes using a classloader and calls "processClass" for each class encountered. This is
 * highly expensive and sometimes dangerous excercise as the classloading may trigger static initializers and may
 * exhaust the "permgen" space of the Virtual machine.
 *
 * If possible anyhow, do not use this class.
 *
 * @author Thomas Morgner
 */
public abstract class ClassQueryTool
{
  /** A logger. */
  private static final Log logger = LogFactory.getLog(ClassQueryTool.class);

  /**
   * The default constructor.
   */
  protected ClassQueryTool()
  {
  }

  /**
   * Processes a single class-file entry. The method will try to load the given entry as java-class and if that
   * successeds will then call the "processClass" method to let the real implementation handle the class.
   *
   * @param classLoader the classloader that should be used for class- and resource loading.
   * @param entryName the file name in the classpath.
   */
  protected void processEntry(final ClassLoader classLoader, final String entryName)
  {
    if (entryName == null)
    {
      throw new NullPointerException();
    }
    if (classLoader == null)
    {
      throw new NullPointerException();
    }

    if (entryName.endsWith(".class") == false)
    {
      return;
    }
    final String className = entryName.substring(0, entryName.length() - 6).replace('/', '.');
    if (isValidClass(className) == false)
    {
      return;
    }
    try
    {
      final Class c = Class.forName(className, false, classLoader);
      processClass(classLoader, c);
    }
    catch (NoClassDefFoundError ndef)
    {
      // Ignore silently. This happens a lot if the classpath is incomplete.
    }
    catch (Throwable e)
    {
      // ignore ..
      logger.debug("At class '" + className + "': " + e);
    }
  }

  /**
   * Checks, whether the class is valid. If the class-name is not considered valid by this method, the class will
   * not be processed. Use this to pre-filter the class-stream as loading classes is expensive.
   *
   * @param className the name of the class.
   * @return true, if the class should be processed, false otherwise.
   */
  protected boolean isValidClass(final String className)
  {
    return true;
  }

  /**
   * The handler method that is called for every class encountered on the classpath.
   *
   * @param classLoader the classloader used to load the class.
   * @param c the class that should be handled.
   */
  protected abstract void processClass(final ClassLoader classLoader, final Class c);

  /**
   * Processes a single jar file. The Jar file is processed in the order of the entries contained within the
   * ZIP-directory.
   *
   * @param classLoader the classloader
   * @param jarFile the URL pointing to the jar file to be parsed.
   */
  private void processJarFile(final ClassLoader classLoader, final URL jarFile)
  {
    try
    {
      final ZipInputStream zf = new ZipInputStream(jarFile.openStream());
      ZipEntry ze;
      while ((ze = zf.getNextEntry()) != null)
      {
        if (!ze.isDirectory())
        {
          processEntry(classLoader, ze.getName());
        }
      }
      zf.close();
    }
    catch (final IOException e1)
    {
      logger.debug("Caught IO-Exception while processing file " + jarFile, e1);
    }
  }

  /**
   * Processes all entries from a given directory, ignoring any subdirectory contents.
   * If the directory contains sub-directories these directories are not searched for JAR or ZIP files.
   * <p/>
   * In addition to the directory given as parameter, the direcories and JAR/ZIP-files on the classpath are also
   * searched for entries.
   * <p/>
   * If directory is null, only the classpath is searched.
   *
   * @param directory the directory to be searched, or null to just use the classpath.
   * @throws IOException if an error occured while loading the resources from the directory.
   * @throws SecurityException if access to the system properties or access to the classloader is restricted.
   * @noinspection AccessOfSystemProperties
   */
  public void processDirectory(final File directory) throws IOException
  {
    final ArrayList allURLs = new ArrayList();
    final ArrayList jarURLs = new ArrayList();
    final ArrayList directoryURLs = new ArrayList();

    final String classpath = System.getProperty("java.class.path");
    final String pathSeparator = System.getProperty("path.separator");
    final StringTokenizer tokenizer = new StringTokenizer(classpath, pathSeparator);

    while (tokenizer.hasMoreTokens())
    {
      final String pathElement = tokenizer.nextToken();

      final File directoryOrJar = new File(pathElement);
      final File file = directoryOrJar.getAbsoluteFile();
      if (file.isDirectory() && file.exists() && file.canRead())
      {
        allURLs.add(file.toURL());
        directoryURLs.add(file);
        continue;
      }

      if (!file.isFile() || (file.exists() == false) || (file.canRead() == false))
      {
        continue;
      }

      final String fileName = file.getName();
      if (fileName.endsWith(".jar") || fileName.endsWith(".zip"))
      {
        allURLs.add(file.toURL());
        jarURLs.add(file.toURL());
      }
    }

    if (directory != null && directory.isDirectory())
    {
      final File[] driverFiles = directory.listFiles();
      for (int i = 0; i < driverFiles.length; i++)
      {
        final File file = driverFiles[i];
        if (file.isDirectory() && file.exists() && file.canRead())
        {
          allURLs.add(file.toURL());
          directoryURLs.add(file);
          continue;
        }

        if (!file.isFile() || (file.exists() == false) || (file.canRead() == false))
        {
          continue;
        }

        final String fileName = file.getName();
        if (fileName.endsWith(".jar") || fileName.endsWith(".zip"))
        {
          allURLs.add(file.toURL());
          jarURLs.add(file.toURL());
        }
      }
    }

    final URL[] urlsArray = (URL[]) jarURLs.toArray(new URL[jarURLs.size()]);
    final File[] dirsArray = (File[]) directoryURLs.toArray(new File[directoryURLs.size()]);
    final URL[] allArray = (URL[]) allURLs.toArray(new URL[allURLs.size()]);

    for (int i = 0; i < allArray.length; i++)
    {
      final URL url = allArray[i];
      logger.debug(url);
    }
    for (int i = 0; i < urlsArray.length; i++)
    {
      final URL url = urlsArray[i];
      final URLClassLoader classLoader = new URLClassLoader(allArray);
      processJarFile(classLoader, url);
    }
    for (int i = 0; i < dirsArray.length; i++)
    {
      final File file = dirsArray[i];
      final URLClassLoader classLoader = new URLClassLoader(allArray);
      processDirectory(classLoader, file, "");
    }
  }

  /**
   * Processes all entries from a given directory. If the directory contains sub-directories these directories
   * are processed in recursive depth-first mannor.
   *
   * @param classLoader the classloader to be used for loading classes.
   * @param file the directory to be searched.
   * @param pathPrefix the path prefix used to construct absolute filenames within the classpath.
   */
  private void processDirectory(final URLClassLoader classLoader, final File file, final String pathPrefix)
  {
    final File[] files = file.listFiles();
    for (int i = 0; i < files.length; i++)
    {
      final File subFile = files[i];
      if (subFile.exists() == false || subFile.canRead() == false)
      {
        continue;
      }

      if (subFile.isDirectory())
      {
        processDirectory(classLoader, subFile, pathPrefix + subFile.getName() + '/');
      }
      else if (subFile.isFile())
      {
        processEntry(classLoader, pathPrefix + subFile.getName());
      }
    }
  }
}
