/*******************************************************************************
 * Copyright (c) 2001, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.core.internal.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.wst.sse.core.internal.Logger;


public class JarUtilities {

	/**
	 * @see http://java.sun.com/products/jsp/errata_1_1_a_042800.html, Issues
	 *      8 & 9
	 * 
	 *      "There are two cases. In both cases the TLD_URI is to be
	 *      interpreted relative to the root of the Web Application. In the
	 *      first case the TLD_URI refers to a TLD file directly. In the
	 *      second case, the TLD_URI refers to a JAR file. If so, that JAR
	 *      file should have a TLD at location META-INF/taglib.tld."
	 */
	public static final String JSP11_TAGLIB = "META-INF/taglib.tld"; //$NON-NLS-1$

	public static void closeJarFile(ZipFile file) {
		if (file == null)
			return;
		try {
			file.close();
		}
		catch (IOException ioe) {
			// no cleanup can be done
		}
	}

	/**
	 * Provides a stream to a local copy of the input or null if not possible
	 */
	protected static InputStream getCachedInputStream(String jarFilename, String entryName) {
		File testFile = new File(jarFilename);
		if (!testFile.exists())
			return getInputStream(ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(jarFilename)), entryName);

		InputStream cache = null;
		ZipFile jarfile = null;
		try {
			jarfile = new ZipFile(jarFilename);
		}
		catch (IOException ioExc) {
			closeJarFile(jarfile);
		}

		if (jarfile != null) {
			try {
				ZipEntry zentry = jarfile.getEntry(entryName);
				if (zentry != null) {
					InputStream entryInputStream = null;
					try {
						entryInputStream = jarfile.getInputStream(zentry);
					}
					catch (IOException ioExc) {
						// no cleanup can be done
					}

					if (entryInputStream != null) {
						int c;
						ByteArrayOutputStream buffer = null;
						if (zentry.getSize() > 0) {
							buffer = new ByteArrayOutputStream((int) zentry.getSize());
						}
						else {
							buffer = new ByteArrayOutputStream();
						}
						// array dim restriction?
						byte bytes[] = new byte[2048];
						try {
							while ((c = entryInputStream.read(bytes)) >= 0) {
								buffer.write(bytes, 0, c);
							}
							cache = new ByteArrayInputStream(buffer.toByteArray());
							closeJarFile(jarfile);
						}
						catch (IOException ioe) {
							// no cleanup can be done
						}
						finally {
							try {
								entryInputStream.close();
							}
							catch (IOException e) {
								// no cleanup can be done
							}
						}
					}
				}
			}
			finally {
				closeJarFile(jarfile);
			}
		}
		return cache;
	}

	private static InputStream copyAndCloseStream(InputStream original) {
		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
		InputStream cachedCopy = null;

		if (original != null) {
			int c;
			// array dim restriction?
			byte bytes[] = new byte[2048];
			try {
				while ((c = original.read(bytes)) >= 0) {
					buffer.write(bytes, 0, c);
				}
				cachedCopy = new ByteArrayInputStream(buffer.toByteArray());
				closeStream(original);
			}
			catch (IOException ioe) {
				// no cleanup can be done
			}
		}
		return cachedCopy;
	}

	/**
	 * @param jarResource
	 *            the zip file
	 * @return a string array containing the entry paths to every file in this
	 *         zip resource, excluding directories
	 */
	public static String[] getEntryNames(IResource jarResource) {
		if (jarResource == null || jarResource.getType() != IResource.FILE || !jarResource.isAccessible())
			return new String[0];

		try {
			return getEntryNames(jarResource.getFullPath().toString(), new ZipInputStream(((IFile) jarResource).getContents()), true);
		}
		catch (CoreException e) {
			// no cleanup can be done
		}

		IPath location = jarResource.getLocation();
		if (location != null)
			return getEntryNames(location.toString());
		return new String[0];
	}

	/**
	 * @param jarFilename
	 *            the location of the zip file
	 * @return a string array containing the entry paths to every file in the
	 *         zip file at this location, excluding directories
	 */
	public static String[] getEntryNames(String jarFilename) {
		return getEntryNames(jarFilename, true);
	}

	private static String[] getEntryNames(String filename, ZipInputStream jarInputStream, boolean excludeDirectories) {
		List entryNames = new ArrayList();
		try {
			ZipEntry z = jarInputStream.getNextEntry();
			while (z != null) {
				if (!(z.isDirectory() && excludeDirectories))
					entryNames.add(z.getName());
				z = jarInputStream.getNextEntry();
			}
		}
		catch (ZipException zExc) {
			Logger.log(Logger.WARNING_DEBUG, "JarUtilities ZipException: (stream) " + filename, zExc); //$NON-NLS-1$ //$NON-NLS-2$
		}
		catch (IOException ioExc) {
			// no cleanup can be done
		}
		finally {
			closeStream(jarInputStream);
		}
		String[] names = (String[]) entryNames.toArray(new String[0]);
		return names;
	}

	private static void closeStream(InputStream inputStream) {
		try {
			inputStream.close();
		}
		catch (IOException e) {
			// no cleanup can be done
		}
	}

	/**
	 * @param jarFilename
	 *            the location of the zip file
	 * @param excludeDirectories
	 *            whether to not include directories in the results
	 * @return a string array containing the entry paths to every file in the
	 *         zip file at this location, excluding directories if indicated
	 */
	public static String[] getEntryNames(String jarFilename, boolean excludeDirectories) {
		ZipFile jarfile = null;
		List entryNames = new ArrayList();
		File f = new File(jarFilename);
		if (f.exists() && f.canRead()) {
			try {
				jarfile = new ZipFile(f);
				Enumeration entries = jarfile.entries();
				while (entries.hasMoreElements()) {
					ZipEntry z = (ZipEntry) entries.nextElement();
					if (!(z.isDirectory() && excludeDirectories))
						entryNames.add(z.getName());
				}
			}
			catch (ZipException zExc) {
				Logger.log(Logger.WARNING_DEBUG, "JarUtilities ZipException: " + jarFilename + " " + zExc.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
			}
			catch (IOException ioExc) {
				// no cleanup can be done
			}
			finally {
				closeJarFile(jarfile);
			}
		}
		String[] names = (String[]) entryNames.toArray(new String[0]);
		return names;
	}

	/**
	 * @param jarResource
	 *            the zip file
	 * @param entryName
	 *            the entry's path in the zip file
	 * @return an InputStream to the contents of the given entry or null if
	 *         not possible
	 */
	public static InputStream getInputStream(IResource jarResource, String entryName) {
		if (jarResource == null || jarResource.getType() != IResource.FILE || !jarResource.isAccessible())
			return null;

		try {
			InputStream zipStream = ((IFile) jarResource).getContents();
			return getInputStream(jarResource.getFullPath().toString(), new ZipInputStream(zipStream), entryName);
		}
		catch (CoreException e) {
			// no cleanup can be done, probably out of sync
		}

		IPath location = jarResource.getLocation();
		if (location != null) {
			return getInputStream(location.toString(), entryName);
		}
		return null;
	}

	private static InputStream getInputStream(String filename, ZipInputStream zip, String entryName) {
		InputStream result = null;
		try {
			ZipEntry z = zip.getNextEntry();
			while (z != null && !z.getName().equals(entryName)) {
				z = zip.getNextEntry();
			}
			if (z != null) {
				result = copyAndCloseStream(zip);
			}
		}
		catch (ZipException zExc) {
			Logger.log(Logger.WARNING_DEBUG, "JarUtilities ZipException: (stream) " + filename, zExc); //$NON-NLS-1$ //$NON-NLS-2$
		}
		catch (IOException ioExc) {
			// no cleanup can be done
		}
		finally {
			closeStream(zip);
		}
		return result;
	}

	/**
	 * @param jarFilename
	 *            the location of the zip file
	 * @param entryName
	 *            the entry's path in the zip file
	 * @return an InputStream to the contents of the given entry or null if
	 *         not possible
	 */
	public static InputStream getInputStream(String jarFilename, String entryName) {
		// check sanity
		if (jarFilename == null || jarFilename.length() < 1 || entryName == null || entryName.length() < 1)
			return null;

		// JAR files are not allowed to have leading '/' in member names
		String internalName = null;
		if (entryName.startsWith("/")) //$NON-NLS-1$
			internalName = entryName.substring(1);
		else
			internalName = entryName;

		return getCachedInputStream(jarFilename, internalName);
	}

	/**
	 * @param url
	 *            a URL pointint to a zip file
	 * @return a cached copy of the contents at this URL, opening it as a file
	 *         if it is a jar:file: URL, and using a URLConnection otherwise,
	 *         or null if it could not be read. All sockets and file handles
	 *         are closed as quickly as possible.
	 */
	public static InputStream getInputStream(URL url) {
		String urlString = url.toString();
		if (urlString.length() > 12 && urlString.startsWith("jar:file:") && urlString.indexOf("!/") > 9) { //$NON-NLS-1$ //$NON-NLS-2$
			int fileIndex = urlString.indexOf("!/"); //$NON-NLS-1$ 
			String jarFileName = urlString.substring(9, fileIndex);
			if (fileIndex < urlString.length()) {
				String jarPath = urlString.substring(fileIndex + 1);
				return getInputStream(jarFileName, jarPath);
			}
		}

		InputStream input = null;
		JarURLConnection jarUrlConnection = null;
		try {
			URLConnection openConnection = url.openConnection();
			openConnection.setDefaultUseCaches(false);
			openConnection.setUseCaches(false);
			if (openConnection instanceof JarURLConnection) {
				jarUrlConnection = (JarURLConnection) openConnection;
				JarFile jarFile = jarUrlConnection.getJarFile();
				input = jarFile.getInputStream(jarUrlConnection.getJarEntry());
			}
			else {
				input = openConnection.getInputStream();
			}
			if (input != null) {
				return copyAndCloseStream(input);
			}
		}
		catch (FileNotFoundException e) {
			// May be a file URL connection, do not log
		}
		catch (IOException e) {
			Logger.logException(e);
		}
		finally {
			if (jarUrlConnection != null) {
				try {
					jarUrlConnection.getJarFile().close();
				}
				catch (IOException e) {
					// ignore
				}
				catch (IllegalStateException e) {
					/*
					 * ignore. Can happen in case the stream.close() did close
					 * the jar file see
					 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=140750
					 */
				}

			}
		}
		return null;
	}
}
