/*
 * JARClassLoader.java - Loads classes from JAR files
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999, 2003 Slava Pestov
 * Portions copyright (C) 1999 mike dillon
 *
 * 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 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 org.gjt.sp.jedit;

//{{{ Imports
import java.io.InputStream;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.gjt.sp.util.Log;
import org.jedit.util.CleanerService;

import java.util.jar.Manifest;
import java.util.jar.JarFile;
import java.net.MalformedURLException;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
//}}}

/**
 * A class loader implementation that loads classes from JAR files. All
 * instances share the same set of classes.
 * @author Slava Pestov
 * @version $Id: JARClassLoader.java 25512 2021-04-25 20:18:15Z kpouer $
 */
public class JARClassLoader extends ClassLoader
{
	//{{{ JARClassLoader constructor
	/**
	 * This constructor creates a class loader for loading classes from all
	 * plugins. For example BeanShell uses one of these so that scripts can
	 * use plugin classes.
	 */
	public JARClassLoader()
	{
		this(true);
	}

	/**
	 * Creates a class loader that will optionally delegate the
	 * finding of classes to the parent class loader by default.
	 *
	 * @since jEdit 4.3pre6
	 */
	public JARClassLoader(boolean delegateFirst)
	{
		this.delegateFirst = delegateFirst;
		// for debugging
		id = INDEX++;
		live++;
		CleanerService.instance.register(this, () -> live--);
	} //}}}

	//{{{ loadClass() method
	/**
	 * @exception ClassNotFoundException if the class could not be found
	 */
	@Override
	public Class loadClass(String clazz, boolean resolveIt)
		throws ClassNotFoundException
	{
		ClassNotFoundException pending = null;
		if (delegateFirst)
		{
			try
			{
				return loadFromParent(clazz);
			}
			catch (ClassNotFoundException cnf)
			{
				// keep going if class was not found.
				pending = cnf;
			}
		}

		Object obj = classHash.get(clazz);
		if(obj == NO_CLASS)
		{
			// we remember which classes we don't exist
			// because BeanShell tries loading all possible
			// <imported prefix>.<class name> combinations
			throw new ClassNotFoundException(clazz);
		}
		else if(obj instanceof JARClassLoader)
		{
			JARClassLoader classLoader = (JARClassLoader)obj;
			try
			{
				return classLoader._loadClass(clazz,resolveIt);
			} catch (ClassNotFoundException cnf2)
			{
				classHash.put(clazz,NO_CLASS);
				throw cnf2;
			}
		}
		else if (delegateFirst)
		{
			// if delegating, reaching this statement means
			// the class was really not found. Otherwise
			// we'll try loading from the parent class loader.
			throw pending;
		}

		return loadFromParent(clazz);
	} //}}}

	//{{{ getResourceAsStream() method
	@Override
	public InputStream getResourceAsStream(String name)
	{
		try
		{
			// try in current jar first
			if(jar != null)
			{
				ZipFile zipFile = jar.getZipFile();
				ZipEntry entry = zipFile.getEntry(name);
				if(entry != null)
				{
					return zipFile.getInputStream(entry);
				}
			}
			// then try from another jar
			Object obj = resourcesHash.get(name);
			if(obj instanceof JARClassLoader)
			{
				JARClassLoader classLoader = (JARClassLoader)obj;
				return classLoader.getResourceAsStream(name);
			}
			// finally try from the system class loader
			return getSystemResourceAsStream(name);
		}
		catch(IOException io)
		{
			Log.log(Log.ERROR,this,io);

			return null;
		}
	} //}}}

	//{{{ getResource() method
	/**
	 * overriding getResource() because we want to search FIRST in this
	 * ClassLoader, then the parent, the path, etc.
	 */
	@Override
	public URL getResource(String name)
	{
		try
		{
			if(jar != null)
			{
				ZipFile zipFile = jar.getZipFile();
				ZipEntry entry = zipFile.getEntry(name);
				if(entry != null)
				{
					return new URL(getResourceAsPath(name));
				}
			}
			
			Object obj = resourcesHash.get(name);
			if(obj instanceof JARClassLoader)
			{
				JARClassLoader classLoader = (JARClassLoader)obj;
				return classLoader.getResource(name);
			} else
			{
				URL ret = getSystemResource(name); 
				if(ret != null)
				{
					Log.log(Log.DEBUG,JARClassLoader.class,"Would have returned null for getResource("+name+")");
					Log.log(Log.DEBUG,JARClassLoader.class,"returning("+ret+")");
				}
				return ret;
			}
		}
		catch(IOException io)
		{
			Log.log(Log.ERROR,this,io);
			return null;
		}
	} //}}}

	//{{{ getResourceAsPath() method
	/**
	 * construct a jeditresource:/etc path from the name
	 * of a resource in the associated jar.
	 * The existence of the resource is not actually checked.
	 *
	 * @param name name of the resource
	 * @return jeditresource:/path_to_the_jar!name_of_the_resource
	 * @throws UnsupportedOperationException if this is an anonymous
	 * JARClassLoader (no associated jar).
	 */
	public String getResourceAsPath(String name)
	{
		// this must be fixed during plugin development
		if(jar == null)
			throw new UnsupportedOperationException(
				"don't call getResourceAsPath() on anonymous JARClassLoader");

		if(!name.startsWith("/"))
			name = '/' + name;

		return "jeditresource:/" + MiscUtilities.getFileName(
			jar.getPath()) + '!' + name;
	} //}}}

	//{{{ dump() method
	/**
	 * For debugging.
	 */
	public static void dump()
	{
		Log.log(Log.DEBUG,JARClassLoader.class,
			"Total instances created: " + INDEX);
		Log.log(Log.DEBUG,JARClassLoader.class,
			"Live instances: " + live);
		synchronized(classHash)
		{
			for (Map.Entry<String, Object> entry : classHash.entrySet())
			{
				if (entry.getValue() != NO_CLASS)
				{
					Log.log(Log.DEBUG, JARClassLoader.class,
						entry.getKey() + " ==> "
							+ entry.getValue());
				}
			}
		}
	} //}}}

	//{{{ toString() method
	public String toString()
	{
		if(jar == null)
			return "<anonymous>(" + id + ')';
		else
			return jar.getPath() + " (" + id + ')';
	} //}}}

	//{{{ findResources() method
	/**
	 * @return zero or one resource, as returned by getResource()
	 */
	@Override
	public Enumeration<URL> getResources(String name) throws IOException
	{
		class SingleElementEnumeration implements Enumeration<URL>
		{
			private URL element;

			SingleElementEnumeration(URL element)
			{
				this.element = element;
			}

			@Override
			public boolean hasMoreElements()
			{
				return element != null;
			}

			@Override
			public URL nextElement()
			{
				if(element != null)
				{
					URL retval = element;
					element = null;
					return retval;
				}
				else
					throw new NoSuchElementException();
			}
		}

		URL resource = getResource(name);
		return new SingleElementEnumeration(resource);
	} //}}}

	//{{{ Package-private members

	//{{{ JARClassLoader constructor
	/**
	 * @since jEdit 4.2pre1
	 */
	JARClassLoader(PluginJAR jar)
	{
		this();
		this.jar = jar;
	} //}}}

	//{{{ activate() method
	void activate()
	{
		if (jar.getPlugin() != null)
		{
			String _delegate = jEdit.getProperty(
				"plugin." + jar.getPlugin().getClassName() + ".class_loader_delegate");
			delegateFirst = _delegate == null || "true".equals(_delegate);
		}

		String[] classes = jar.getClasses();
		if(classes != null)
		{
			for (String aClass : classes)
				classHash.put(aClass, this);
		}

		String[] resources = jar.getResources();
		if(resources != null)
		{
			for (String resource : resources)
				resourcesHash.put(resource, this);
		}
	} //}}}

	//{{{ deactivate() method
	void deactivate()
	{
		String[] classes = jar.getClasses();
		if(classes != null)
		{
			for (String aClass : classes)
			{
				Object loader = classHash.get(aClass);
				if (loader == this)
					classHash.remove(aClass);
				/* else two plugins provide same class! */
			}
		}

		String[] resources = jar.getResources();
		if(resources == null)
			return;

		for (String resource : resources)
		{
			Object loader = resourcesHash.get(resource);
			if (loader == this)
				resourcesHash.remove(resource);
			/* else two plugins provide same resource! */
		}
	} //}}}

	//}}}

	//{{{ Private members

	// used to mark non-existent classes in class hash
	private static final Object NO_CLASS = new Object();

	private static int INDEX;
	private static int live;
	private static final Map<String, Object> classHash = new Hashtable<>();
	private static final Map<String, Object> resourcesHash = new HashMap<>();

	private final int id;
	private boolean delegateFirst;
	private PluginJAR jar;

	//{{{ _loadClass() method
	/**
	 * Load class from this JAR only.
	 */
	private synchronized Class _loadClass(String clazz, boolean resolveIt)
		throws ClassNotFoundException
	{
		jar.activatePlugin();

		synchronized(this)
		{
			Class cls = findLoadedClass(clazz);
			if(cls != null)
			{
				if(resolveIt)
					resolveClass(cls);
				return cls;
			}

			String name = MiscUtilities.classToFile(clazz);

			try
			{
				definePackage(clazz);
				ZipFile zipFile = jar.getZipFile();
				ZipEntry entry = zipFile.getEntry(name);

				if(entry == null)
					throw new ClassNotFoundException(clazz);

				InputStream in = zipFile.getInputStream(entry);

				int len = (int)entry.getSize();
				byte[] data = new byte[len];
				int success = 0;
				int offset = 0;
				while(success < len)
				{
					len -= success;
					offset += success;
					success = in.read(data,offset,len);
					if(success == -1)
					{
						Log.log(Log.ERROR,this,"Failed to load class "
							+ clazz + " from " + zipFile.getName());
						throw new ClassNotFoundException(clazz);
					}
				}

				cls = defineClass(clazz,data,0,data.length);

				if(resolveIt)
					resolveClass(cls);

				return cls;
			}
			catch(IOException io)
			{
				Log.log(Log.ERROR,this,io);

				throw new ClassNotFoundException(clazz);
			}
		}
	} //}}}

	//{{{ definePackage(clazz) method
	private void definePackage(String clazz) throws IOException
	{
		int idx = clazz.lastIndexOf('.');
		if (idx != -1)
		{
			String name = clazz.substring(0, idx);
			if (getDefinedPackage(name) == null) definePackage(name, new JarFile(jar.getFile()).getManifest());
		}
	} //}}}

	//{{{ getMfValue() method
	private static String getMfValue(Attributes sectionAttrs, Attributes mainAttrs, Attributes.Name name)
	{
		String value=null;
		if (sectionAttrs != null)
			value = sectionAttrs.getValue(name);
		else if (mainAttrs != null)
		{
			value = mainAttrs.getValue(name);
		}
		return value;
	}
	//}}}

	//{{{ definePackage(packageName, manifest) method
	private void definePackage(String name, Manifest mf)
	{
		if (mf==null)
		{
			definePackage(name, null, null, null, null, null,
			null, null);
			return;
		}

		Attributes sa = mf.getAttributes(name.replace('.', '/') + '/');
		Attributes ma = mf.getMainAttributes();

		URL sealBase = null;
		if (Boolean.parseBoolean(getMfValue(sa, ma, Name.SEALED)))
		{
			try
			{
				sealBase = jar.getFile().toURI().toURL();
			}
			catch (MalformedURLException e) {}	// NOPMD
		}

		definePackage(
			name,
			getMfValue(sa, ma, Name.SPECIFICATION_TITLE),
			getMfValue(sa, ma, Name.SPECIFICATION_VERSION),
			getMfValue(sa, ma, Name.SPECIFICATION_VENDOR),
			getMfValue(sa, ma, Name.IMPLEMENTATION_TITLE),
			getMfValue(sa, ma, Name.IMPLEMENTATION_VERSION),
			getMfValue(sa, ma, Name.IMPLEMENTATION_VENDOR),
			sealBase);
	} //}}}

	//{{{ loadFromParent() method
	private Class loadFromParent(String clazz)
		throws ClassNotFoundException
	{
		Class cls;

		ClassLoader parentLoader = getClass().getClassLoader();
		if (parentLoader != null)
			cls = parentLoader.loadClass(clazz);
		else
			cls = findSystemClass(clazz);

		return cls;
	} //}}}

	//}}}
}
