/*
 * freenet - JarClassLoader.java Copyright © 2007 David Roden
 * 
 * This program 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 of the License, or (at your option) any later
 * version.
 * 
 * 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 General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package freenet.support;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

import freenet.support.io.FileUtil;

/**
 * Class loader that loads classes from a JAR file. The JAR file gets copied
 * to a temporary location; requests for classes and resources from this class
 * loader are then satisfied from this local copy.
 * 
 * @author <a href="mailto:dr@ina-germany.de">David Roden</a>
 * @version $Id$
 */
public class JarClassLoader extends ClassLoader implements Closeable {
	private static volatile boolean logMINOR;
	static {
		Logger.registerClass(JarClassLoader.class);
	}

	/** The temporary jar file. */
	private JarFile tempJarFile;
	
	/**
	 * Constructs a new jar class loader that loads classes from the jar file
	 * with the given name in the local file system.
	 * 
	 * @param fileName
	 *            The name of the jar file
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public JarClassLoader(String fileName) throws IOException {
		this(new File(fileName));
	}

	/**
	 * Constructs a new jar class loader that loads classes from the specified
	 * URL.
	 * 
	 * @param fileUrl
	 *            The URL to load the jar file from
	 * @param length
	 *            The length of the jar file if known, <code>-1</code>
	 *            otherwise
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public JarClassLoader(URL fileUrl, long length) throws IOException {
		copyFileToTemp(fileUrl.openStream(), length);
	}

	/**
	 * Constructs a new jar class loader that loads classes from the specified
	 * file.
	 * 
	 * @param file
	 *            The file to load classes from
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public JarClassLoader(File file) throws IOException {
		tempJarFile = new JarFile(file);
	}

	/**
	 * Copies the contents of the input stream (which are supposed to be the
	 * contents of a jar file) to a temporary location.
	 * 
	 * @param inputStream
	 *            The input stream to read from
	 * @param length
	 *            The length of the stream if known, <code>-1</code> if the
	 *            length is not known
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	private void copyFileToTemp(InputStream inputStream, long length) throws IOException {
		File tempFile = File.createTempFile("jar-", ".tmp");
		FileOutputStream fileOutputStream = new FileOutputStream(tempFile);
		FileUtil.copy(inputStream, fileOutputStream, length);
		fileOutputStream.close();
		tempFile.deleteOnExit();
		tempJarFile = new JarFile(tempFile);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * This method searches the temporary copy of the jar file for an entry
	 * that is specified by the given class name.
	 * 
	 * @see java.lang.ClassLoader#findClass(java.lang.String)
	 */
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		try {
			String pathName = transformName(name);
			JarEntry jarEntry = tempJarFile.getJarEntry(pathName);
			if (jarEntry != null) {
				long size = jarEntry.getSize();
				InputStream jarEntryInputStream = tempJarFile.getInputStream(jarEntry);
				ByteArrayOutputStream classBytesOutputStream = new ByteArrayOutputStream((int) size);
				FileUtil.copy(jarEntryInputStream, classBytesOutputStream, size);
				classBytesOutputStream.close();
				jarEntryInputStream.close();
				byte[] classBytes = classBytesOutputStream.toByteArray();

				definePackage(name);
					
				Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);
				return clazz;
			}
			throw new ClassNotFoundException("could not find jar entry for class " + name);
		} catch (IOException e) {
			throw new ClassNotFoundException(e.getMessage(), e);
		}
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * Finds the resource within this jar only. If it isn't found within the jar, getResourceAsStream()
	 * will look elsewhere.
	 */
	@Override
	protected URL findResource(String name) {
		/* FIXME compatibility code. remove when all plugins are fixed. */
		if (name.startsWith("/")) {
			name = name.substring(1);
		}
		try {
			if(tempJarFile.getJarEntry(name)==null) {
				return null;
			}

			return new URL("jar:" + new File(tempJarFile.getName()).toURI().toURL() + "!/" + name);
		} catch (MalformedURLException e) {
		}
		return null;
	}

	@Override
	protected Enumeration<URL> findResources(String name) {
		return new Enumeration<URL>() {
			private final Enumeration<JarEntry> jarFileEntries = tempJarFile.entries();
			private URL nextElement = null;

			@Override
			public boolean hasMoreElements() {
				if (nextElement != null) {
					return true;
				}
				while ((nextElement == null) && jarFileEntries.hasMoreElements()) {
					JarEntry jarEntry = jarFileEntries.nextElement();
					if (jarEntry.getName().equals(name)) {
						try {
							nextElement = new URL("jar:" + new File(tempJarFile.getName()).toURI().toURL() + "!/" + name);
						} catch (MalformedURLException e) {
							/* ignore. */
						}
					}
				}
				return nextElement != null;
			}

			@Override
			public URL nextElement() {
				if (!hasMoreElements()) {
					throw new NoSuchElementException();
				}
				URL elementToReturn = nextElement;
				nextElement = null;
				return elementToReturn;
			}
		};
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * If the resource is found in this jar, opens the stream using ZipEntry's,
	 * so when tempJarFile is closed, so are all the streams, hence we can delete
	 * the jar on Windows.
	 * 
	 * @see java.lang.ClassLoader#getResourceAsStream(java.lang.String)
	 */
	@Override
	public InputStream getResourceAsStream(String name) {
		if(logMINOR) Logger.minor(this, "Requested resource: " + name, new Exception("debug"));
		URL url = getResource(name);
		if (url == null)
			return null;
		if(logMINOR) Logger.minor(this, "Found resource at URL: " + url);

		// If the resource is not from our jar, return it as normal
		URL localUrl = findResource(name);
		if (localUrl == null || !url.toString().equals(localUrl.toString()))
			try {
				return url.openStream();
			} catch (IOException e) {
				return null;
			}

		// If the resource is from our jar, open InputStream explicitly from the jar
		// so that we can close() all opened streams later and let the jar file
		// to be deleted on Windows

		/* FIXME compatibility code. remove when all plugins are fixed. */
		if (name.startsWith("/")) {
			name = name.substring(1);
		}

		ZipEntry entry = tempJarFile.getEntry(name);
		try {
			return entry != null ? tempJarFile.getInputStream(entry) : null;
		} catch (IOException e) {
			return null;
		}
	}

	/**
	 * Transforms the class name into a file name that can be used to locate
	 * an entry in the jar file.
	 * 
	 * @param name
	 *            The name of the class
	 * @return The path name of the entry in the jar file
	 */
	private String transformName(String name) {
		return name.replace('.', '/') + ".class";
	}
	
	protected Package definePackage(String name) throws IllegalArgumentException {
		Package pkg = null;
		int i = name.lastIndexOf('.');
		if (i != -1) {
			String pkgname = name.substring(0, i);
			pkg = getPackage(pkgname);
			if (pkg == null) {
				try {
					Manifest man = tempJarFile.getManifest();
					if(man == null) throw new IOException();
					pkg = definePackage(pkgname, man);
				} catch (IOException e) {
					pkg = definePackage(pkgname, null, null, null, null, null, null, null);
				}
			}
		}
		return pkg;
	}

	protected Package definePackage(String name, Manifest man) throws IllegalArgumentException {
		String path = name.replace('.', '/').concat("/");
		String specTitle = null, specVersion = null, specVendor = null;
		String implTitle = null, implVersion = null, implVendor = null;
		String sealed = null;
		URL sealBase = null;

		Attributes attr = man.getAttributes(path);
		if (attr != null) {
			specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
			specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
			specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
			implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
			implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
			implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
			sealed = attr.getValue(Name.SEALED);
		}
		attr = man.getMainAttributes();
		if (attr != null) {
			if (specTitle == null) {
				specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
			}
			if (specVersion == null) {
				specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
			}
			if (specVendor == null) {
				specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
			}
			if (implTitle == null) {
				implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
			}
			if (implVersion == null) {
				implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
			}
			if (implVendor == null) {
				implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
			}
			if (sealed == null) {
				sealed = attr.getValue(Name.SEALED);
			}
		}
		return definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
	}
	
	@Override
	public void close() throws IOException {
		tempJarFile.close();
	}
}
