package junit.runner;

import java.util.*;
import java.io.*;
import java.net.URL;
import java.util.zip.*;

/**
 * A custom class loader which enables the reloading
 * of classes for each test run. The class loader
 * can be configured with a list of package paths that
 * should be excluded from loading. The loading
 * of these packages is delegated to the system class
 * loader. They will be shared across test runs.
 * <p>
 * The list of excluded package paths is specified in
 * a properties file "excluded.properties" that is located in 
 * the same place as the TestCaseClassLoader class.
 * <p>
 * <b>Known limitation:</b> the TestCaseClassLoader cannot load classes
 * from jar files.
 */


public class TestCaseClassLoader extends ClassLoader {
	/** scanned class path */
	private Vector<String> fPathItems;
	/** default excluded paths */
	private String[] defaultExclusions= {
		"junit.framework.", 
		"junit.extensions.", 
		"junit.runner."
	};
	/** name of excluded properties file */
	static final String EXCLUDED_FILE= "excluded.properties";
	/** excluded paths */
	private Vector<String> fExcluded;
	 
	/**
	 * Constructs a TestCaseLoader. It scans the class path
	 * and the excluded package paths
	 */
	public TestCaseClassLoader() {
		this(System.getProperty("java.class.path"));
	}
	
	/**
	 * Constructs a TestCaseLoader. It scans the class path
	 * and the excluded package paths
	 */
	public TestCaseClassLoader(String classPath) {
		scanPath(classPath);
		readExcludedPackages();
	}

	private void scanPath(String classPath) {
		String separator= System.getProperty("path.separator");
		fPathItems= new Vector<String>(10);
		StringTokenizer st= new StringTokenizer(classPath, separator);
		while (st.hasMoreTokens()) {
			fPathItems.addElement(st.nextToken());
		}
	}
	
	public URL getResource(String name) {
		return ClassLoader.getSystemResource(name);
	}
	
	public InputStream getResourceAsStream(String name) {
		return ClassLoader.getSystemResourceAsStream(name);
	} 
	
	public boolean isExcluded(String name) {
		for (int i= 0; i < fExcluded.size(); i++) {
			if (name.startsWith((String) fExcluded.elementAt(i))) {
				return true;
			}
		}
		return false;	
	}
	
	public synchronized Class loadClass(String name, boolean resolve)
		throws ClassNotFoundException {
			
		Class c= findLoadedClass(name);
		if (c != null)
			return c;
		//
		// Delegate the loading of excluded classes to the
		// standard class loader.
		//
		if (isExcluded(name)) {
			try {
				c= findSystemClass(name);
				return c;
			} catch (ClassNotFoundException e) {
				// keep searching
			}
		}
		if (c == null) {
			byte[] data= lookupClassData(name);
			if (data == null)
				throw new ClassNotFoundException();
			c= defineClass(name, data, 0, data.length);
		}
		if (resolve) 
			resolveClass(c);
		return c;
	}
	
	private byte[] lookupClassData(String className) throws ClassNotFoundException {
		byte[] data= null;
		for (int i= 0; i < fPathItems.size(); i++) {
			String path= (String) fPathItems.elementAt(i);
			String fileName= className.replace('.', '/')+".class";
			if (isJar(path)) {
				data= loadJarData(path, fileName);
			} else {
				data= loadFileData(path, fileName);
			}
			if (data != null)
				return data;
		}
		throw new ClassNotFoundException(className);
	}
		
	boolean isJar(String pathEntry) {
		return pathEntry.endsWith(".jar") ||
                       pathEntry.endsWith(".zip") ||
                       pathEntry.endsWith(".apk");
	}

	private byte[] loadFileData(String path, String fileName) {
		File file= new File(path, fileName);
		if (file.exists()) { 
			return getClassData(file);
		}
		return null;
	}
	
	private byte[] getClassData(File f) {
		try {
			FileInputStream stream= new FileInputStream(f);
			ByteArrayOutputStream out= new ByteArrayOutputStream(1000);
			byte[] b= new byte[1000];
			int n;
			while ((n= stream.read(b)) != -1) 
				out.write(b, 0, n);
			stream.close();
			out.close();
			return out.toByteArray();

		} catch (IOException e) {
		}
		return null;
	}

	private byte[] loadJarData(String path, String fileName) {
		ZipFile zipFile= null;
		InputStream stream= null;
		File archive= new File(path);
		if (!archive.exists())
			return null;
		try {
			zipFile= new ZipFile(archive);
		} catch(IOException io) {
			return null;
		}
		ZipEntry entry= zipFile.getEntry(fileName);
		if (entry == null)
			return null;
		int size= (int) entry.getSize();
		try {
			stream= zipFile.getInputStream(entry);
			byte[] data= new byte[size];
			int pos= 0;
			while (pos < size) {
				int n= stream.read(data, pos, data.length - pos);
				pos += n;
			}
			zipFile.close();
			return data;
		} catch (IOException e) {
		} finally {
			try {
				if (stream != null)
					stream.close();
			} catch (IOException e) {
			}
		}
		return null;
	}
	
	private void readExcludedPackages() {		
		fExcluded= new Vector<String>(10);
		for (int i= 0; i < defaultExclusions.length; i++)
			fExcluded.addElement(defaultExclusions[i]);
			
		InputStream is= getClass().getResourceAsStream(EXCLUDED_FILE);
		if (is == null) 
			return;
		Properties p= new Properties();
		try {
			p.load(is);
		}
		catch (IOException e) {
			return;
		} finally {
			try {
				is.close();
			} catch (IOException e) {
			}
		}
		for (Enumeration e= p.propertyNames(); e.hasMoreElements(); ) {
			String key= (String)e.nextElement();
			if (key.startsWith("excluded.")) {
				String path= p.getProperty(key);
				path= path.trim();
				if (path.endsWith("*"))
					path= path.substring(0, path.length()-1);
				if (path.length() > 0) 
					fExcluded.addElement(path);				
			}
		}
	}
}
