/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2001 - 2023 Cloud Software Group, Inc. All rights reserved.
 * http://www.jaspersoft.com
 *
 * Unless you have purchased a commercial license agreement from Jaspersoft,
 * the following license terms apply:
 *
 * This program is part of JasperReports.
 *
 * JasperReports is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JasperReports 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
 */
package net.sf.jasperreports.engine.util;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRVirtualizationHelper;
import net.sf.jasperreports.engine.JRVirtualizer;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReportsContext;


/**
 * Utility class that helps load serialized objects found in various locations 
 * such as files, URLs, and input streams.
 * <p>
 * Many JasperReports processes, like report compilation, report filling and exporting,
 * often work with serialized objects. Sometimes it is useful to manually load those
 * serialized objects before submitting them to the desired JasperReport process.
 * </p><p>
 * The most interesting method exposed by this class is
 * {@link #getLocationInputStream(String) getLocationInputStream(String location)}. 
 * When calling this method to load an InputStream object from
 * the supplied location, the program first tries to interpret the location as a valid resource name. If
 * this fails, then the program assumes that the supplied location is the name of a file on
 * disk and tries to read from it. If no file is found at that location, it will assume that the location 
 * represents a valid URL. Only after this third
 * try fails an exception is thrown.
 * </p>
 * @author Teodor Danciu (teodord@users.sourceforge.net)
 */
public final class JRLoader
{

	private static final Log log = LogFactory.getLog(JRLoader.class);
	public static final String EXCEPTION_MESSAGE_KEY_BYTE_DATA_FROM_INPUT_STREAM_ERROR = "util.loader.byte.data.from.input.stream.error";
	public static final String EXCEPTION_MESSAGE_KEY_BYTE_DATA_LOADING_ERROR = "util.loader.byte.data.loading.error";
	public static final String EXCEPTION_MESSAGE_KEY_CLASS_NOT_FOUND_FROM_FILE = "util.loader.class.not.found.from.file";
	public static final String EXCEPTION_MESSAGE_KEY_CLASS_NOT_FOUND_FROM_INPUT_STREAM = "util.loader.class.not.found.from.input.stream";
	public static final String EXCEPTION_MESSAGE_KEY_CLASS_NOT_FOUND_FROM_URL = "util.loader.class.not.found.from.url";
	public static final String EXCEPTION_MESSAGE_KEY_FILE_OPEN_ERROR = "util.loader.file.open.error";
	public static final String EXCEPTION_MESSAGE_KEY_INPUT_STREAM_FROM_FILE_OPEN_ERROR = "util.loader.input.stream.from.file.open.error";
	public static final String EXCEPTION_MESSAGE_KEY_INPUT_STREAM_FROM_URL_OPEN_ERROR = "util.loader.input.stream.from.url.open.error";
	public static final String EXCEPTION_MESSAGE_KEY_OBJECT_FROM_FILE_LOADING_ERROR = "util.loader.object.from.file.loading.error";
	public static final String EXCEPTION_MESSAGE_KEY_OBJECT_FROM_INPUT_STREAM_LOADING_ERROR = "util.loader.object.from.input.stream.loading.error";
	public static final String EXCEPTION_MESSAGE_KEY_OBJECT_FROM_URL_LOADING_ERROR = "util.loader.object.from.url.loading.error";
	public static final String EXCEPTION_MESSAGE_KEY_RESOURCE_NOT_FOUND = "util.loader.resource.not.found";
	public static final String EXCEPTION_MESSAGE_KEY_URL_OPEN_ERROR = "util.loader.url.open.error";

	/**
	 *
	 */
	//private static boolean wasWarning;


	/**
	 *
	 */
	public static Object loadObjectFromFile(String fileName) throws JRException
	{
		return loadObject(new File(fileName));
	}


	/**
	 *
	 */
	public static Object loadObject(File file) throws JRException
	{
		return loadObject(DefaultJasperReportsContext.getInstance(), file);
	}


	/**
	 *
	 */
	public static Object loadObject(JasperReportsContext jasperReportsContext, File file) throws JRException
	{
		if (!file.exists() || !file.isFile())
		{
			throw new JRException( new FileNotFoundException(String.valueOf(file)) );
		}

		Object obj = null;

		try (
			InputStream is = new BufferedInputStream(new FileInputStream(file));
			ObjectInputStream ois = new ContextClassLoaderObjectInputStream(jasperReportsContext, is)
			)
		{
			obj = ois.readObject();
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_OBJECT_FROM_FILE_LOADING_ERROR,
					new Object[]{file},
					e);
		}
		catch (ClassNotFoundException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_CLASS_NOT_FOUND_FROM_FILE,
					new Object[]{file},
					e);
		}

		return obj;
	}


	/**
	 *
	 */
	public static Object loadObject(URL url) throws JRException
	{
		return loadObject(DefaultJasperReportsContext.getInstance(), url);
	}


	/**
	 *
	 */
	public static Object loadObject(JasperReportsContext jasperReportsContext, URL url) throws JRException
	{
		Object obj = null;

		try (
			InputStream is = url.openStream();
			ObjectInputStream ois = new ContextClassLoaderObjectInputStream(jasperReportsContext, is)
			)
		{
			obj = ois.readObject();
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_OBJECT_FROM_URL_LOADING_ERROR,
					new Object[]{url},
					e);
		}
		catch (ClassNotFoundException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_CLASS_NOT_FOUND_FROM_URL,
					new Object[]{url},
					e);
		}

		return obj;
	}


	/**
	 *
	 */
	public static Object loadObject(InputStream is) throws JRException
	{
		return loadObject(DefaultJasperReportsContext.getInstance(), is);
	}


	/**
	 *
	 */
	public static Object loadObject(JasperReportsContext jasperReportsContext, InputStream is) throws JRException
	{
		Object obj = null;

		ObjectInputStream ois = null;

		try
		{
			ois = new ContextClassLoaderObjectInputStream(jasperReportsContext, is);
			obj = ois.readObject();
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_OBJECT_FROM_INPUT_STREAM_LOADING_ERROR,
					null,
					e);
		}
		catch (ClassNotFoundException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_CLASS_NOT_FOUND_FROM_INPUT_STREAM,
					null,
					e);
		}

		return obj;
	}


	/**
	 *
	 */
	public static InputStream getInputStream(File file) throws JRException
	{
		if (!file.exists() || !file.isFile())
		{
			throw new JRException( new FileNotFoundException(String.valueOf(file)) );//FIXMEREPO this probably useless
		}

		FileInputStream fis = null;

		try
		{
			fis = new FileInputStream(file);
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_INPUT_STREAM_FROM_FILE_OPEN_ERROR,
					new Object[]{file},
					e);
		}

		return fis;
	}


	/**
	 *
	 */
	public static InputStream getInputStream(URL url) throws JRException
	{
		InputStream is = null;

		try
		{
			is = url.openStream();
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_INPUT_STREAM_FROM_URL_OPEN_ERROR,
					new Object[]{url},
					e);
		}

		return is;
	}


	/**
	 *
	 */
	public static byte[] loadBytes(File file) throws JRException
	{
		try
		{
			return Files.readAllBytes(file.toPath());
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_BYTE_DATA_LOADING_ERROR,
					new Object[]{file},
					e);
		}
	}


	/**
	 *
	 */
	public static byte[] loadBytes(URL url) throws JRException
	{
		try (InputStream is = url.openStream())
		{
			return readBytes(is);
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_BYTE_DATA_LOADING_ERROR,
					new Object[]{url},
					e);
		}
	}


	/**
	 *
	 */
	public static byte[] loadBytes(InputStream is) throws JRException
	{
		try
		{
			return readBytes(is);
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_BYTE_DATA_FROM_INPUT_STREAM_ERROR,
					null,
					e);
		}
	}


	/**
	 *
	 */
	public static byte[] readBytes(InputStream is) throws IOException
	{
		try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
		{
			byte[] bytes = new byte[10000];
			int ln = 0;
			while ((ln = is.read(bytes)) > 0)
			{
				baos.write(bytes, 0, ln);
			}

			baos.flush();

			return baos.toByteArray();
		}
	}

	/**
	 *
	 */
	public static byte[] loadBytesFromResource(String resourceName) throws JRException
	{
		return loadBytesFromResource(resourceName, null);
	}


	/**
	 *
	 */
	public static byte[] loadBytesFromResource(String resourceName, ClassLoader classLoader) throws JRException
	{
		URL url = JRResourcesUtil.findClassLoaderResource(resourceName, classLoader);
		if (url != null)
		{
			return loadBytes(url);
		}
		throw 
			new JRException(
				EXCEPTION_MESSAGE_KEY_RESOURCE_NOT_FOUND,
				new Object[]{resourceName});
	}
		
	
	/**
	 * Tries to open an input stream for a location.
	 * <p>
	 * The method tries to interpret the location as a file name, a resource name or
	 * an URL.  If any of these succeed, an input stream is created and returned.
	 * 
	 * @param location the location
	 * @return an input stream if the location is an existing file name, a resource name on
	 * the classpath or an URL or <code>null</code> otherwise.
	 * 
	 * @throws JRException
	 */
	public static InputStream getLocationInputStream(String location) throws JRException//FIXME deprecate this?
	{
		InputStream is = null;
		
		is = getResourceInputStream(location);
		
		if (is == null)
		{
			is = getFileInputStream(location);
		}
		
		if (is == null)
		{
			is = getURLInputStream(location);
		}
		
		return is;
	}


	/**
	 * Tries to open a file for reading.
	 * 
	 * @param filename the file name
	 * @return an input stream for the file or <code>null</code> if the file was not found
	 * @throws JRException
	 */
	public static InputStream getFileInputStream(String filename) throws JRException
	{
		InputStream is = null;
		
		File file = new File(filename);
		if (file.exists() && file.isFile())
		{
			try
			{
				is = new FileInputStream(file);
			}
			catch (FileNotFoundException e)
			{
				throw 
					new JRException(
						EXCEPTION_MESSAGE_KEY_FILE_OPEN_ERROR,
						new Object[]{filename},
						e);
			}
		}
		
		return is;
	}


	/**
	 * Tries to open an input stream for a resource.
	 *  
	 * @param resource the resource name
	 * @return an input stream for the resource or <code>null</code> if the resource was not found
	 */
	public static InputStream getResourceInputStream(String resource)
	{
		InputStream is = null;
		
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();		
		if (classLoader != null)
		{
			is = classLoader.getResourceAsStream(resource);
		}
		
		if (is == null)
		{
			classLoader = JRLoader.class.getClassLoader();
			if (classLoader != null)
			{
				is = classLoader.getResourceAsStream(resource);
			}
			
			if (is == null)
			{
				is = JRPropertiesUtil.class.getResourceAsStream("/" + resource);
			}
		}

		return is;
	}

	/**
	 * Scans the context classloader and the classloader of this class for all 
	 * resources that have a specified name, and returns a list of
	 * {@link URL}s for the found resources.
	 * 
	 * @param resource the resource names
	 * @return a list of {@link URL}s of resources with the specified name;
	 * the list is empty if no resources have been found for the name
	 * @see ClassLoader#getResources(String)
	 */
	public static List<URL> getResources(String resource)
	{
		//skip duplicated resources
		Set<URL> resources = new LinkedHashSet<>();
		collectResources(resource, JRLoader.class.getClassLoader(), 
				resources);
		collectResources(resource, Thread.currentThread().getContextClassLoader(), 
				resources);
		return new ArrayList<>(resources);
	}


	protected static void collectResources(String resourceName,
			ClassLoader classLoader, Set<URL> resources)
	{
		if (classLoader != null)
		{
			try
			{
				Enumeration<URL> urls = classLoader.getResources(resourceName);
				if (urls != null)
				{
					while (urls.hasMoreElements()) // class loaders should never return null on getResources, according to specs, but we've seen cases, so we protect our code here
					{
						URL url = urls.nextElement();
						resources.add(url);
					}
				}
			}
			catch (IOException e)
			{
				throw new JRRuntimeException(e);
			}
		}
	}

	/**
	 * Scans the context classloader and the classloader of this class for all 
	 * resources that have a specified name, and returns a list of
	 * {@link ClassLoaderResource} objects for the found resources.
	 * 
	 * <p>
	 * The returned list contains the URLs of the resources, and for each resource
	 * the highest classloader in the classloader hierarchy on which the resource
	 * was found.
	 * </p>
	 * 
	 * @param resource the resource names
	 * @return a list of resources with the specified name;
	 * the list is empty if no resources have been found for the name
	 * @see ClassLoader#getResources(String)
	 */
	public static List<ClassLoaderResource> getClassLoaderResources(
			String resource)
	{
		Map<URL, ClassLoaderResource> resources = new LinkedHashMap<>();
		collectResources(resource, JRLoader.class.getClassLoader(), resources);
		//TODO check if the classloader is the same
		collectResources(resource, Thread.currentThread()
				.getContextClassLoader(), resources);
		return new ArrayList<>(resources.values());
	}

	protected static void collectResources(String resourceName,
			ClassLoader classLoader, Map<URL, ClassLoaderResource> resources)
	{
		if (classLoader == null)
		{
			return;
		}
		
		try
		{
			// creating a list of parent classloaders, with the highest in the
			// hierarchy first
			LinkedList<ClassLoader> classloaders = new LinkedList<>();
			ClassLoader ancestorLoader = classLoader;
			while (ancestorLoader != null)
			{
				classloaders.addFirst(ancestorLoader);
				
				try
				{
					ancestorLoader = ancestorLoader.getParent();
				}
				catch (SecurityException e)
				{
					// if we're not allowed to get the parent, stop here.
					// resources will be listed with the first classloader that
					// we're allowed to access.
					// one case when this happens is applets.
					// FIXME revisit logging on SecurityException for applet
					ancestorLoader = null;
				}
			}

			for (ClassLoader ancestor : classloaders)
			{
				Enumeration<URL> urls = ancestor.getResources(resourceName);
				if (urls != null) // class loaders should never return null on getResources, according to specs, but we've seen cases, so we protect our code here
				{
					while (urls.hasMoreElements())
					{
						URL url = urls.nextElement();
						// if this is the first time we see this resource, add it
						// with the current classloader.
						// this way a resource will be added with the most first
						// ancestor classloader that has it.
						if (!resources.containsKey(url))
						{
							if (log.isDebugEnabled())
							{
								log.debug("Found resource " + resourceName 
										+ " at "+ url + " in classloader " + ancestor);
							}
							
							ClassLoaderResource resource = new ClassLoaderResource(
									url, ancestor);
							resources.put(url, resource);
						}
					}
				}
			}
		} catch (IOException e)
		{
			throw new JRRuntimeException(e);
		}
	}

	/**
	 * Returns the resource URL for a specified resource name.
	 * 
	 * @param resource the resource name
	 * @return the URL of the resource having the specified name, or
	 * <code>null</code> if none found
	 * @see ClassLoader#getResource(String)
	 */
	public static URL getResource(String resource)
	{
		URL location = null;
		
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();		
		if (classLoader != null)
		{
			location = classLoader.getResource(resource);
		}
		
		if (location == null)
		{
			classLoader = JRLoader.class.getClassLoader();
			if (classLoader != null)
			{
				location = classLoader.getResource(resource);
			}
			
			if (location == null)
			{
				location = JRPropertiesUtil.class.getResource("/" + resource);
			}
		}

		return location;
	}

	/**
	 * Tries to open an input stream for an URL.
	 * 
	 * @param spec the string to parse as an URL
	 * @return an input stream for the URL or null if <code>spec</code> is not a valid URL
	 * @throws JRException
	 */
	public static InputStream getURLInputStream(String spec) throws JRException
	{
		InputStream is = null;
		
		try
		{
			URL url = new URL(spec);
			is = url.openStream();
		}
		catch (MalformedURLException e)
		{
		}
		catch (IOException e)
		{
			throw 
				new JRException(
					EXCEPTION_MESSAGE_KEY_URL_OPEN_ERROR,
					new Object[]{spec},
					e);
		}
		
		return is;
	}
	
	/**
	 * Loads a JasperPrint object from a file, optionally using a virtualizer for
	 * the object.
	 * 
	 * @param fileName the file name
	 * @param virtualizer the virtualizer
	 * @return a JasperPrint object
	 * @throws JRException
	 * @see JRVirtualizationHelper#setThreadVirtualizer(JRVirtualizer)
	 */
	public static JasperPrint loadJasperPrintFromFile(String fileName, JRVirtualizer virtualizer) throws JRException
	{
		if (virtualizer != null)
		{
			JRVirtualizationHelper.setThreadVirtualizer(virtualizer);
		}
		try
		{
			return (JasperPrint) loadObjectFromFile(fileName);
		}
		finally
		{
			if (virtualizer != null)
			{
				JRVirtualizationHelper.clearThreadVirtualizer();
			}
		}
	}
	
	/**
	 * Loads a JasperPrint object from a file, optionally using a virtualizer for
	 * the object.
	 * 
	 * @param file the file
	 * @param virtualizer the virtualizer
	 * @return a JasperPrint object
	 * @throws JRException
	 * @see JRVirtualizationHelper#setThreadVirtualizer(JRVirtualizer)
	 */
	public static JasperPrint loadJasperPrint(File file, JRVirtualizer virtualizer) throws JRException
	{
		if (virtualizer != null)
		{
			JRVirtualizationHelper.setThreadVirtualizer(virtualizer);
		}
		try
		{
			return (JasperPrint) loadObject(file);
		}
		finally
		{
			if (virtualizer != null)
			{
				JRVirtualizationHelper.clearThreadVirtualizer();
			}
		}
	}

	/**
	 * Loads a JasperPrint object from a URL, optionally using a virtualizer for
	 * the object.
	 * 
	 * @param url the URL
	 * @param virtualizer the virtualizer
	 * @return a JasperPrint object
	 * @throws JRException
	 * @see JRVirtualizationHelper#setThreadVirtualizer(JRVirtualizer)
	 */
	public static JasperPrint loadJasperPrint(URL url, JRVirtualizer virtualizer) throws JRException
	{
		if (virtualizer != null)
		{
			JRVirtualizationHelper.setThreadVirtualizer(virtualizer);
		}
		try
		{
			return (JasperPrint) loadObject(url);
		}
		finally
		{
			if (virtualizer != null)
			{
				JRVirtualizationHelper.clearThreadVirtualizer();
			}
		}
	}
	
	/**
	 * Loads a JasperPrint object from a stream, optionally using a virtualizer for
	 * the object.
	 * 
	 * @param is the stream
	 * @param virtualizer the virtualizer
	 * @return a JasperPrint object
	 * @throws JRException
	 * @see JRVirtualizationHelper#setThreadVirtualizer(JRVirtualizer)
	 */
	public static JasperPrint loadJasperPrint(InputStream is, JRVirtualizer virtualizer) throws JRException
	{
		if (virtualizer != null)
		{
			JRVirtualizationHelper.setThreadVirtualizer(virtualizer);
		}
		try
		{
			return (JasperPrint) loadObject(is);
		}
		finally
		{
			if (virtualizer != null)
			{
				JRVirtualizationHelper.clearThreadVirtualizer();
			}
		}
	}
	
	private JRLoader()
	{
	}
}
