/*******************************************************************************
 * Copyright (c) 2006 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:
 *     Matthew Webster - initial implementation
 *******************************************************************************/
package org.aspectj.weaver.loadtime;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;

import com.bea.jvm.ClassPreProcessor;
import com.bea.jvm.JVMFactory;

import junit.framework.TestCase;

public class JRockitAgentTest extends TestCase {

	protected void setUp() throws Exception {
		super.setUp();
	}

	protected void tearDown() throws Exception {
		super.tearDown();
	}

	public void testJRockitAgent() {
		ClassPreProcessor preProcessor = new JRockitAgent();
		ClassPreProcessor expectedPreProcessor = JVMFactory.getJVM().getClassLibrary().getClassPreProcessor();
		assertEquals("JRocketAgent must be registered", expectedPreProcessor, preProcessor);
	}

	public void testPreProcess() {
		ClassPreProcessor preProcessor = new JRockitAgent();
		preProcessor.preProcess(null, "foo.Bar", new byte[] {});
	}

	public void testJrockitRecursionProtection() {
		if (LangUtil.is11VMOrGreater()) {
			// Skip test, not castable to URLClassLoader
			return;
		}
		URLClassLoader thisLoader = (URLClassLoader) getClass().getClassLoader();
		URL jrockit = FileUtil.getFileURL(new File("../lib/ext/jrockit/jrockit.jar"));
		URL[] urls = new URL[] {jrockit};
		 thisLoader = new URLClassLoader(urls, thisLoader);
		ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();

		try {
			/* Needed by Commons Logging */
			Thread.currentThread().setContextClassLoader(thisLoader.getParent());

			ClassLoader loader = new JRockitClassLoader(thisLoader);

			Class.forName("java.lang.Object", false, loader);
			Class.forName("junit.framework.TestCase", false, loader);
		} catch (Exception ex) {
			ex.printStackTrace();
			fail(ex.toString());
		} finally {
			Thread.currentThread().setContextClassLoader(contextLoader);
		}
	}

	private class JRockitClassLoader extends ClassLoader {

		public final static boolean debug = false;

		private List path = new LinkedList();
		// private com.bea.jvm.ClassPreProcessor agent;
		private Object agent;
		private Method preProcess;

		public JRockitClassLoader(URLClassLoader clone) throws Exception {
			/* Use extensions loader */
			super(clone.getParent());

			URL[] urls = clone.getURLs();
			for (int i = 0; i < urls.length; i++) {
				Object pathElement;
				URL url = urls[i];
				if (debug)
					System.out.println("JRockitClassLoader.JRockitClassLoader() url=" + url.getPath());
				File file = new File(encode(url.getFile()));
				if (debug)
					System.out.println("JRockitClassLoader.JRockitClassLoader() file" + file);
				if (file.isDirectory())
					pathElement = file;
				else if (file.exists() && file.getName().endsWith(".jar"))
					pathElement = new JarFile(file);
				else
					throw new RuntimeException(file.getAbsolutePath());
				path.add(pathElement);
			}

			Class agentClazz = Class.forName("org.aspectj.weaver.loadtime.JRockitAgent", false, this);
			Object obj = agentClazz.newInstance();
			if (debug)
				System.out.println("JRockitClassLoader.JRockitClassLoader() obj=" + obj);
			this.agent = obj;
			byte[] bytes = new byte[] {};
			Class[] parameterTypes = new Class[] { java.lang.ClassLoader.class, java.lang.String.class, bytes.getClass() };
			preProcess = agentClazz.getMethod("preProcess", parameterTypes);
		}

		/* Get rid of escaped characters */
		private String encode(String s) {
			StringBuffer result = new StringBuffer();
			int i = s.indexOf("%");
			while (i != -1) {
				result.append(s.substring(0, i));
				String escaped = s.substring(i + 1, i + 3);
				s = s.substring(i + 3);
				Integer value = Integer.valueOf(escaped, 16);
				result.append(new Character((char) value.intValue()));
				i = s.indexOf("%");
			}
			result.append(s);
			return result.toString();
		}

		protected Class findClass(String name) throws ClassNotFoundException {
			if (debug)
				System.out.println("> JRockitClassLoader.findClass() name=" + name);
			Class clazz = null;
			try {
				clazz = super.findClass(name);
			} catch (ClassNotFoundException ex) {
				for (Iterator i = path.iterator(); clazz == null && i.hasNext();) {
					byte[] classBytes = null;
					try {
						Object pathElement = i.next();
						if (pathElement instanceof File) {
							File dir = (File) pathElement;
							String className = name.replace('.', '/') + ".class";
							File classFile = new File(dir, className);
							if (classFile.exists())
								classBytes = loadClassFromFile(name, classFile);
						} else {
							JarFile jar = (JarFile) pathElement;
							String className = name.replace('.', '/') + ".class";
							ZipEntry entry = jar.getEntry(className);
							if (entry != null)
								classBytes = loadBytesFromZipEntry(jar, entry);
						}

						if (classBytes != null) {
							clazz = defineClass(name, classBytes);
						}
					} catch (IOException ioException) {
						ex.printStackTrace();
					}
				}
			}

			if (debug)
				System.out.println("< JRockitClassLoader.findClass() name=" + name);
			return clazz;
		}

		private Class defineClass(String name, byte[] bytes) {
			if (debug)
				System.out.println("> JRockitClassLoader.defineClass() name=" + name);
			try {
				if (agent != null)
					preProcess.invoke(agent, new Object[] { this, name, bytes });
			} catch (IllegalAccessException iae) {
				iae.printStackTrace();
				throw new ClassFormatError(iae.getMessage());
			} catch (InvocationTargetException ite) {
				ite.printStackTrace();
				throw new ClassFormatError(ite.getTargetException().getMessage());
			}
			if (debug)
				System.out.println("< JRockitClassLoader.defineClass() name=" + name);
			return super.defineClass(name, bytes, 0, bytes.length);
		}

		private byte[] loadClassFromFile(String name, File file) throws IOException {
			if (debug)
				System.out.println("JRockitClassLoader.loadClassFromFile() file=" + file);

			byte[] bytes;
			bytes = new byte[(int) file.length()];
			FileInputStream fis = null;
			try {
				fis = new FileInputStream(file);
				bytes = readBytes(fis, bytes);
			} finally {
				if (fis != null)
					fis.close();
			}

			return bytes;
		}

		private byte[] loadBytesFromZipEntry(JarFile jar, ZipEntry entry) throws IOException {
			if (debug)
				System.out.println("JRockitClassLoader.loadBytesFromZipEntry() entry=" + entry);

			byte[] bytes;
			bytes = new byte[(int) entry.getSize()];
			InputStream is = null;
			try {
				is = jar.getInputStream(entry);
				bytes = readBytes(is, bytes);
			} finally {
				if (is != null)
					is.close();
			}

			return bytes;
		}

		private byte[] readBytes(InputStream is, byte[] bytes) throws IOException {
			for (int offset = 0; offset < bytes.length;) {
				int read = is.read(bytes, offset, bytes.length - offset);
				offset += read;
			}
			return bytes;
		}
	}
}
