/* gnu.classpath.tools.FileSystemClassLoader
   Copyright (C) 2004 Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath 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 2, or (at your option)
any later version.

GNU Classpath 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 GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */

package gnu.classpath.tools;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;

import java.net.MalformedURLException;
import java.net.URL;

import java.util.LinkedList;
import java.util.List;
import java.util.ArrayList;
import java.util.StringTokenizer;

import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.jar.Attributes;

/**
 *  A <code>ClassLoader</code> implementation which looks for classes
 *  on the local filesystem given a standard search path.
 */
public class FileSystemClassLoader extends ClassLoader {

   private File[] pathComponents;

   /**
    *  Initialize the class loader with a normal path string. The path
    *  string should contain path components separated by {@link
    *  File.pathSeparator}. Each path component should either denote a
    *  directory or a .jar or .zip file.
    */
   public FileSystemClassLoader(String path)
   {
      List components = new ArrayList();
      for (StringTokenizer st = new StringTokenizer(path, File.pathSeparator); st.hasMoreTokens(); ) {
         File pathComponent = new File(st.nextToken());
         components.add(pathComponent);
         if (pathComponent.exists() && !pathComponent.isDirectory()) {
            List subComponents = tryGetJarFileClassPathComponents(pathComponent);
            if (null != subComponents) {
               components.addAll(subComponents);
            }
         }
      }
      File[] componentArray = new File[components.size()];
      this.pathComponents = (File[])components.toArray(componentArray);
   }

   /**
    *  Initialize the class loader with an array of path
    *  components. Each path component should either denote a
    *  directory or a .jar or .zip file.
    */
   public FileSystemClassLoader(File[] pathComponents)
   {
      this.pathComponents = pathComponents;
      for (int i = 0; i < pathComponents.length; ++i) {
         if (!pathComponents[i].exists()) {
            System.err.println("WARNING: Path component '" + pathComponents[i] + "' not found.");
         }
      }
   }

   public Class loadClass(String name)
      throws ClassNotFoundException {

      return super.loadClass(name);
   }

   public Class findClass(String name)
      throws ClassNotFoundException {

      byte[] b = loadClassData(name);
      return defineClass(name, b, 0, b.length);
   }

   public URL findResource(String name)
   {
      StreamInfo streamInfo = getResourceStream(name);
      if (null == streamInfo) {
         return super.findResource(name);
      }
      else {
         try {
            return streamInfo.getURL();
         }
         catch (MalformedURLException e) {
            System.err.println("WARNING: In FileSystemClassLoader: could not derive URL from file or jar entry: " + e.toString());
            return null;
         }
      }
   }

   private byte[] readFromStream(InputStream in, long size)
      throws IOException
   {
      byte[] result = new byte[(int)size];
      int nread = 0;
      int offset = 0;
      while (offset < size && (nread = in.read(result, offset, (int)(size - offset))) >= 0) {
         offset += nread;
      }
      in.close();
      return result;
   }

   private byte[] readFromStream(StreamInfo streamInfo)
      throws IOException
   {
      InputStream in = streamInfo.openStream();
      long size = streamInfo.getSize();

      byte[] result = new byte[(int)size];
      int nread = 0;
      int offset = 0;
      while (offset < size && (nread = in.read(result, offset, (int)(size - offset))) >= 0) {
         offset += nread;
      }
      in.close();
      return result;
   }

   private static interface StreamInfo
   {
      public InputStream openStream()
         throws IOException;
      public long getSize();
      public URL getURL()
         throws MalformedURLException;
   }

   private static class FileStreamInfo
      implements StreamInfo
   {
      File file;

      FileStreamInfo(File file)
      {
         this.file = file;
      }

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

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

      public URL getURL()
         throws MalformedURLException
      {
         return file.toURL();
      }
   }

   private static class JarStreamInfo
      implements StreamInfo
   {
      private File file;
      private JarFile jarFile;
      private JarEntry jarEntry;

      JarStreamInfo(File file, JarFile jarFile, JarEntry jarEntry)
      {
         this.file = file;
         this.jarFile = jarFile;
         this.jarEntry = jarEntry;
      }

      public InputStream openStream()
         throws IOException
      {
         return jarFile.getInputStream(jarEntry);
      }

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

      public URL getURL()
         throws MalformedURLException
      {
         String urlString = "jar:" + file.toURL() + "!/" + jarEntry.getName();
         return new URL(urlString);
      }
   }

   private StreamInfo getResourceStream(String path)
   {
      for (int i = 0; i < pathComponents.length; ++i) {
         try {
            File parent = pathComponents[i];
            if (parent.isDirectory()) {
               File file = new File(parent, path);
               if (file.exists()) {
                  return new FileStreamInfo(file);
               }
            }
            else {
               JarFile jarFile = new JarFile(parent, false, JarFile.OPEN_READ);
               JarEntry jarEntry = jarFile.getJarEntry(path);
               if (null != jarEntry) {
                  return new JarStreamInfo(parent, jarFile, jarEntry);
               }
            }
         }
         catch (IOException ignore) {
         }
      }
      return null;
   }

   private byte[] loadClassData(String className)
      throws ClassNotFoundException
   {
      String classFileName = className.replace('.', File.separatorChar) + ".class";
      StreamInfo streamInfo = getResourceStream(classFileName);

      try {
         if (null != streamInfo) {
            return readFromStream(streamInfo);
         }
      }
      catch (IOException ignore) {
      }

      throw new ClassNotFoundException(className);
   }

   private static List tryGetJarFileClassPathComponents(File file)
   {
      try {
         JarFile jarFile = new JarFile(file, false, JarFile.OPEN_READ);
         Manifest manifest = jarFile.getManifest();
         if (null != manifest) {
            Attributes mainAttributes = manifest.getMainAttributes();
            if (null != mainAttributes) {
               String classPath = mainAttributes.getValue(Attributes.Name.CLASS_PATH);
               if (null != classPath) {
                  List result = new LinkedList();
                  StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(classPath));
                  tokenizer.resetSyntax();
                  tokenizer.wordChars(0, Integer.MAX_VALUE);
                  tokenizer.whitespaceChars(9, 9);   // tab
                  tokenizer.whitespaceChars(10, 10); // lf
                  tokenizer.whitespaceChars(13, 13); // cr
                  tokenizer.whitespaceChars(32, 32); // space
                  tokenizer.quoteChar('"');
                  int token;
                  while ((token = tokenizer.nextToken()) != StreamTokenizer.TT_EOF) {
                     if (StreamTokenizer.TT_WORD == token) {
                        result.add(new File(file.getParentFile(), tokenizer.sval));
                     }
                  }
                  return result;
               }
            }
         }
      }
      catch (IOException ignore) {
      }
      return null;
   }
}
