/*******************************************************************************
 * Copyright (c) 2008, 2009 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

/**
 * The java.net.URLClassLoader class allows one to load resources from arbitrary URLs and in particular is optimized to handle
 * "jar" URLs. Unfortunately for jar files this optimization ends up holding the file open which ultimately prevents the file from
 * being deleted or update until the VM is shutdown.
 *   
 * The CloseableURLClassLoader is meant to replace the URLClassLoader and provides an additional method to allow one to "close" any
 * resources left open. In the current version the CloseableURLClassLoader will only ensure the closing of jar file resources. The
 * jar handling behavior in this class will also provides a construct to allow one to turn off jar file verification in performance
 * sensitive situations where the verification us not necessary. 
 * 
 * also see https://bugs.eclipse.org/bugs/show_bug.cgi?id=190279 
 */

package org.eclipse.equinox.servletbridge;

import java.io.*;
import java.net.*;
import java.security.*;
import java.util.*;
import java.util.jar.*;
import java.util.jar.Attributes.Name;

public class CloseableURLClassLoader extends URLClassLoader {
	static final String DOT_CLASS = ".class"; //$NON-NLS-1$
	static final String BANG_SLASH = "!/"; //$NON-NLS-1$
	static final String JAR = "jar"; //$NON-NLS-1$

	// @GuardedBy("loaders")
	final ArrayList loaders = new ArrayList(); // package private to avoid synthetic access.
	// @GuardedBy("loaders")
	private final ArrayList loaderURLs = new ArrayList(); // note: protected by loaders
	// @GuardedBy("loaders")
	boolean closed = false; // note: protected by loaders, package private to avoid synthetic access.

	private final AccessControlContext context;
	private final boolean verifyJars;

	private static class CloseableJarURLConnection extends JarURLConnection {
		private final JarFile jarFile;
		// @GuardedBy("this")
		private JarEntry entry;

		public CloseableJarURLConnection(URL url, JarFile jarFile) throws MalformedURLException {
			super(url);
			this.jarFile = jarFile;
		}

		public void connect() throws IOException {
			internalGetEntry();
		}

		private synchronized JarEntry internalGetEntry() throws IOException {
			if (entry != null)
				return entry;
			entry = jarFile.getJarEntry(getEntryName());
			if (entry == null)
				throw new FileNotFoundException(getEntryName());
			return entry;
		}

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

		/**
		 * @throws IOException
		 * Documented to avoid warning 
		 */
		public JarFile getJarFile() throws IOException {
			return jarFile;
		}

		public JarEntry getJarEntry() throws IOException {
			return internalGetEntry();
		}
	}

	private static class CloseableJarURLStreamHandler extends URLStreamHandler {
		private final JarFile jarFile;

		public CloseableJarURLStreamHandler(JarFile jarFile) {
			this.jarFile = jarFile;
		}

		protected URLConnection openConnection(URL u) throws IOException {
			return new CloseableJarURLConnection(u, jarFile);
		}

		protected void parseURL(URL u, String spec, int start, int limit) {
			setURL(u, JAR, null, 0, null, null, spec.substring(start, limit), null, null);
		}
	}

	private static class CloseableJarFileLoader {
		private final JarFile jarFile;
		private final Manifest manifest;
		private final CloseableJarURLStreamHandler jarURLStreamHandler;
		private final String jarFileURLPrefixString;

		public CloseableJarFileLoader(File file, boolean verify) throws IOException {
			this.jarFile = new JarFile(file, verify);
			this.manifest = jarFile.getManifest();
			this.jarURLStreamHandler = new CloseableJarURLStreamHandler(jarFile);
			this.jarFileURLPrefixString = file.toURL().toString() + BANG_SLASH;
		}

		public URL getURL(String name) {
			if (jarFile.getEntry(name) != null)
				try {
					return new URL(JAR, null, -1, jarFileURLPrefixString + name, jarURLStreamHandler);
				} catch (MalformedURLException e) {
					// ignore
				}
			return null;
		}

		public Manifest getManifest() {
			return manifest;
		}

		public void close() {
			try {
				jarFile.close();
			} catch (IOException e) {
				// ignore
			}
		}
	}

	/**
	 * @param urls the array of URLs to use for loading resources
	 * @see URLClassLoader
	 */
	public CloseableURLClassLoader(URL[] urls) {
		this(urls, ClassLoader.getSystemClassLoader(), true);
	}

	/**
	 * @param urls the URLs from which to load classes and resources
	 * @param parent the parent class loader used for delegation
	 * @see URLClassLoader
	 */
	public CloseableURLClassLoader(URL[] urls, ClassLoader parent) {
		this(excludeFileJarURLS(urls), parent, true);
	}

	/**
	 * @param urls the URLs from which to load classes and resources
	 * @param parent the parent class loader used for delegation
	 * @param verifyJars flag to determine if jar file verification should be performed
	 * @see URLClassLoader
	 */
	public CloseableURLClassLoader(URL[] urls, ClassLoader parent, boolean verifyJars) {
		super(excludeFileJarURLS(urls), parent);
		this.context = AccessController.getContext();
		this.verifyJars = verifyJars;
		for (int i = 0; i < urls.length; i++) {
			if (isFileJarURL(urls[i])) {
				loaderURLs.add(urls[i]);
				safeAddLoader(urls[i]);
			}
		}
	}

	// @GuardedBy("loaders")
	private void safeAddLoader(URL url) {
		String path = url.getPath();
		File file = new File(path);
		if (file.exists()) {
			try {
				loaders.add(new CloseableJarFileLoader(file, verifyJars));
			} catch (IOException e) {
				// ignore
			}
		}
	}

	private static URL[] excludeFileJarURLS(URL[] urls) {
		ArrayList urlList = new ArrayList();
		for (int i = 0; i < urls.length; i++) {
			if (!isFileJarURL(urls[i]))
				urlList.add(urls[i]);
		}
		return (URL[]) urlList.toArray(new URL[urlList.size()]);
	}

	private static boolean isFileJarURL(URL url) {
		if (!url.getProtocol().equals("file")) //$NON-NLS-1$
			return false;

		String path = url.getPath();
		if (path != null && path.endsWith("/")) //$NON-NLS-1$
			return false;

		return true;
	}

	/* (non-Javadoc)
	 * @see java.net.URLClassLoader#findClass(java.lang.String)
	 */
	protected Class findClass(final String name) throws ClassNotFoundException {
		try {
			Class clazz = (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() {
				public Object run() throws ClassNotFoundException {
					String resourcePath = name.replace('.', '/') + DOT_CLASS;
					CloseableJarFileLoader loader = null;
					URL resourceURL = null;
					synchronized (loaders) {
						if (closed)
							return null;
						for (Iterator iterator = loaders.iterator(); iterator.hasNext();) {
							loader = (CloseableJarFileLoader) iterator.next();
							resourceURL = loader.getURL(resourcePath);
							if (resourceURL != null)
								break;
						}
					}
					if (resourceURL != null) {
						try {
							return defineClass(name, resourceURL, loader.getManifest());
						} catch (IOException e) {
							throw new ClassNotFoundException(name, e);
						}
					}
					return null;
				}
			}, context);
			if (clazz != null)
				return clazz;
		} catch (PrivilegedActionException e) {
			throw (ClassNotFoundException) e.getException();
		}
		return super.findClass(name);
	}

	// package private to avoid synthetic access.
	Class defineClass(String name, URL resourceURL, Manifest manifest) throws IOException {
		JarURLConnection connection = (JarURLConnection) resourceURL.openConnection();
		int lastDot = name.lastIndexOf('.');
		if (lastDot != -1) {
			String packageName = name.substring(0, lastDot + 1);
			Package pkg = getPackage(packageName);
			if (pkg != null) {
				checkForSealedPackage(pkg, packageName, manifest, connection.getJarFileURL());
			} else {
				definePackage(packageName, manifest, connection.getJarFileURL());
			}
		}
		JarEntry entry = connection.getJarEntry();
		byte[] bytes = new byte[(int) entry.getSize()];
		DataInputStream is = null;
		try {
			is = new DataInputStream(connection.getInputStream());
			is.readFully(bytes, 0, bytes.length);
			CodeSource cs = new CodeSource(connection.getJarFileURL(), entry.getCertificates());
			return defineClass(name, bytes, 0, bytes.length, cs);
		} finally {
			if (is != null)
				try {
					is.close();
				} catch (IOException e) {
					// ignore
				}
		}
	}

	private void checkForSealedPackage(Package pkg, String packageName, Manifest manifest, URL jarFileURL) {
		if (pkg.isSealed() && !pkg.isSealed(jarFileURL))
			throw new SecurityException("The package '" + packageName + "' was previously loaded and is already sealed."); //$NON-NLS-1$ //$NON-NLS-2$

		String entryPath = packageName.replace('.', '/') + "/"; //$NON-NLS-1$
		Attributes entryAttributes = manifest.getAttributes(entryPath);
		String sealed = null;
		if (entryAttributes != null)
			sealed = entryAttributes.getValue(Name.SEALED);

		if (sealed == null) {
			Attributes mainAttributes = manifest.getMainAttributes();
			if (mainAttributes != null)
				sealed = mainAttributes.getValue(Name.SEALED);
		}
		if (Boolean.valueOf(sealed).booleanValue())
			throw new SecurityException("The package '" + packageName + "' was previously loaded unsealed. Cannot seal package."); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/* (non-Javadoc)
	 * @see java.net.URLClassLoader#findResource(java.lang.String)
	 */
	public URL findResource(final String name) {
		URL url = (URL) AccessController.doPrivileged(new PrivilegedAction() {
			public Object run() {
				synchronized (loaders) {
					if (closed)
						return null;
					for (Iterator iterator = loaders.iterator(); iterator.hasNext();) {
						CloseableJarFileLoader loader = (CloseableJarFileLoader) iterator.next();
						URL resourceURL = loader.getURL(name);
						if (resourceURL != null)
							return resourceURL;
					}
				}
				return null;
			}
		}, context);
		if (url != null)
			return url;
		return super.findResource(name);
	}

	/* (non-Javadoc)
	 * @see java.net.URLClassLoader#findResources(java.lang.String)
	 */
	public Enumeration findResources(final String name) throws IOException {
		final List resources = new ArrayList();
		AccessController.doPrivileged(new PrivilegedAction() {
			public Object run() {
				synchronized (loaders) {
					if (closed)
						return null;
					for (Iterator iterator = loaders.iterator(); iterator.hasNext();) {
						CloseableJarFileLoader loader = (CloseableJarFileLoader) iterator.next();
						URL resourceURL = loader.getURL(name);
						if (resourceURL != null)
							resources.add(resourceURL);
					}
				}
				return null;
			}
		}, context);
		Enumeration e = super.findResources(name);
		while (e.hasMoreElements())
			resources.add(e.nextElement());

		return Collections.enumeration(resources);
	}

	/**
	 * The "close" method is called when the class loader is no longer needed and we should close any open resources.
	 * In particular this method will close the jar files associated with this class loader.
	 */
	public void close() {
		synchronized (loaders) {
			if (closed)
				return;
			for (Iterator iterator = loaders.iterator(); iterator.hasNext();) {
				CloseableJarFileLoader loader = (CloseableJarFileLoader) iterator.next();
				loader.close();
			}
			closed = true;
		}
	}

	/* (non-Javadoc)
	 * @see java.net.URLClassLoader#addURL(java.net.URL)
	 */
	protected void addURL(URL url) {
		synchronized (loaders) {
			if (isFileJarURL(url)) {
				if (closed)
					throw new IllegalStateException("Cannot add url. CloseableURLClassLoader is closed."); //$NON-NLS-1$
				loaderURLs.add(url);
				safeAddLoader(url);
				return;
			}
		}
		super.addURL(url);
	}

	/* (non-Javadoc)
	 * @see java.net.URLClassLoader#getURLs()
	 */
	public URL[] getURLs() {
		List result = new ArrayList();
		synchronized (loaders) {
			result.addAll(loaderURLs);
		}
		result.addAll(Arrays.asList(super.getURLs()));
		return (URL[]) result.toArray(new URL[result.size()]);
	}
}
