/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.loader.jar;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;

import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.data.RandomAccessDataFile;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

/**
 * Tests for {@link JarFile}.
 *
 * @author Phillip Webb
 * @author Martin Lau
 * @author Andy Wilkinson
 */
public class JarFileTests {

	private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";

	private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	@Rule
	public TemporaryFolder temporaryFolder = new TemporaryFolder();

	private File rootJarFile;

	private JarFile jarFile;

	@Before
	public void setup() throws Exception {
		this.rootJarFile = this.temporaryFolder.newFile();
		TestJarCreator.createTestJar(this.rootJarFile);
		this.jarFile = new JarFile(this.rootJarFile);
	}

	@Test
	public void jdkJarFile() throws Exception {
		// Sanity checks to see how the default jar file operates
		java.util.jar.JarFile jarFile = new java.util.jar.JarFile(this.rootJarFile);
		Enumeration<java.util.jar.JarEntry> entries = jarFile.entries();
		assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
		assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
		assertThat(entries.nextElement().getName()).isEqualTo("1.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("2.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("d/");
		assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("special/");
		assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
		assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
		assertThat(entries.hasMoreElements()).isFalse();
		URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/");
		URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl });
		assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull();
		assertThat(urlClassLoader.getResource("d/9.dat")).isNotNull();
		jarFile.close();
		urlClassLoader.close();
	}

	@Test
	public void createFromFile() throws Exception {
		JarFile jarFile = new JarFile(this.rootJarFile);
		assertThat(jarFile.getName()).isNotNull();
		jarFile.close();
	}

	@Test
	public void getManifest() throws Exception {
		assertThat(this.jarFile.getManifest().getMainAttributes().getValue("Built-By"))
				.isEqualTo("j1");
	}

	@Test
	public void getManifestEntry() throws Exception {
		ZipEntry entry = this.jarFile.getJarEntry("META-INF/MANIFEST.MF");
		Manifest manifest = new Manifest(this.jarFile.getInputStream(entry));
		assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1");
	}

	@Test
	public void getEntries() throws Exception {
		Enumeration<java.util.jar.JarEntry> entries = this.jarFile.entries();
		assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
		assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
		assertThat(entries.nextElement().getName()).isEqualTo("1.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("2.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("d/");
		assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("special/");
		assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
		assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
		assertThat(entries.hasMoreElements()).isFalse();
	}

	@Test
	public void getSpecialResourceViaClassLoader() throws Exception {
		URLClassLoader urlClassLoader = new URLClassLoader(
				new URL[] { this.jarFile.getUrl() });
		assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull();
		urlClassLoader.close();
	}

	@Test
	public void getJarEntry() throws Exception {
		java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat");
		assertThat(entry).isNotNull();
		assertThat(entry.getName()).isEqualTo("1.dat");
	}

	@Test
	public void getInputStream() throws Exception {
		InputStream inputStream = this.jarFile
				.getInputStream(this.jarFile.getEntry("1.dat"));
		assertThat(inputStream.available()).isEqualTo(1);
		assertThat(inputStream.read()).isEqualTo(1);
		assertThat(inputStream.available()).isEqualTo(0);
		assertThat(inputStream.read()).isEqualTo(-1);
	}

	@Test
	public void getName() throws Exception {
		assertThat(this.jarFile.getName()).isEqualTo(this.rootJarFile.getPath());
	}

	@Test
	public void getSize() throws Exception {
		assertThat(this.jarFile.size()).isEqualTo((int) this.rootJarFile.length());
	}

	@Test
	public void getEntryTime() throws Exception {
		java.util.jar.JarFile jdkJarFile = new java.util.jar.JarFile(this.rootJarFile);
		assertThat(this.jarFile.getEntry("META-INF/MANIFEST.MF").getTime())
				.isEqualTo(jdkJarFile.getEntry("META-INF/MANIFEST.MF").getTime());
		jdkJarFile.close();
	}

	@Test
	public void close() throws Exception {
		RandomAccessDataFile randomAccessDataFile = spy(
				new RandomAccessDataFile(this.rootJarFile, 1));
		JarFile jarFile = new JarFile(randomAccessDataFile);
		jarFile.close();
		verify(randomAccessDataFile).close();
	}

	@Test
	public void getUrl() throws Exception {
		URL url = this.jarFile.getUrl();
		assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/");
		JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
		assertThat(jarURLConnection.getJarFile()).isSameAs(this.jarFile);
		assertThat(jarURLConnection.getJarEntry()).isNull();
		assertThat(jarURLConnection.getContentLength()).isGreaterThan(1);
		assertThat(jarURLConnection.getContent()).isSameAs(this.jarFile);
		assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar");
		assertThat(jarURLConnection.getJarFileURL().toURI())
				.isEqualTo(this.rootJarFile.toURI());
	}

	@Test
	public void createEntryUrl() throws Exception {
		URL url = new URL(this.jarFile.getUrl(), "1.dat");
		assertThat(url.toString())
				.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/1.dat");
		JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
		assertThat(jarURLConnection.getJarFile()).isSameAs(this.jarFile);
		assertThat(jarURLConnection.getJarEntry())
				.isSameAs(this.jarFile.getJarEntry("1.dat"));
		assertThat(jarURLConnection.getContentLength()).isEqualTo(1);
		assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class);
		assertThat(jarURLConnection.getContentType()).isEqualTo("content/unknown");
		assertThat(jarURLConnection.getPermission()).isInstanceOf(FilePermission.class);
		FilePermission permission = (FilePermission) jarURLConnection.getPermission();
		assertThat(permission.getActions()).isEqualTo("read");
		assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath());
	}

	@Test
	public void getMissingEntryUrl() throws Exception {
		URL url = new URL(this.jarFile.getUrl(), "missing.dat");
		assertThat(url.toString())
				.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/missing.dat");
		this.thrown.expect(FileNotFoundException.class);
		((JarURLConnection) url.openConnection()).getJarEntry();
	}

	@Test
	public void getUrlStream() throws Exception {
		URL url = this.jarFile.getUrl();
		url.openConnection();
		this.thrown.expect(IOException.class);
		url.openStream();
	}

	@Test
	public void getEntryUrlStream() throws Exception {
		URL url = new URL(this.jarFile.getUrl(), "1.dat");
		url.openConnection();
		InputStream stream = url.openStream();
		assertThat(stream.read()).isEqualTo(1);
		assertThat(stream.read()).isEqualTo(-1);
	}

	@Test
	public void getNestedJarFile() throws Exception {
		JarFile nestedJarFile = this.jarFile
				.getNestedJarFile(this.jarFile.getEntry("nested.jar"));

		Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
		assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
		assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
		assertThat(entries.nextElement().getName()).isEqualTo("3.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("4.dat");
		assertThat(entries.nextElement().getName()).isEqualTo("\u00E4.dat");
		assertThat(entries.hasMoreElements()).isFalse();

		InputStream inputStream = nestedJarFile
				.getInputStream(nestedJarFile.getEntry("3.dat"));
		assertThat(inputStream.read()).isEqualTo(3);
		assertThat(inputStream.read()).isEqualTo(-1);

		URL url = nestedJarFile.getUrl();
		assertThat(url.toString())
				.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/");
		JarURLConnection conn = (JarURLConnection) url.openConnection();
		assertThat(conn.getJarFile()).isSameAs(nestedJarFile);
		assertThat(conn.getJarFileURL().toString())
				.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
		assertThat(conn.getInputStream()).isNotNull();
		JarInputStream jarInputStream = new JarInputStream(conn.getInputStream());
		assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("3.dat");
		assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("4.dat");
		assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("\u00E4.dat");
		jarInputStream.close();
		assertThat(conn.getPermission()).isInstanceOf(FilePermission.class);
		FilePermission permission = (FilePermission) conn.getPermission();
		assertThat(permission.getActions()).isEqualTo("read");
		assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath());
	}

	@Test
	public void getNestedJarDirectory() throws Exception {
		JarFile nestedJarFile = this.jarFile
				.getNestedJarFile(this.jarFile.getEntry("d/"));

		Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
		assertThat(entries.nextElement().getName()).isEqualTo("9.dat");
		assertThat(entries.hasMoreElements()).isFalse();

		InputStream inputStream = nestedJarFile
				.getInputStream(nestedJarFile.getEntry("9.dat"));
		assertThat(inputStream.read()).isEqualTo(9);
		assertThat(inputStream.read()).isEqualTo(-1);

		URL url = nestedJarFile.getUrl();
		assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/d!/");
		assertThat(((JarURLConnection) url.openConnection()).getJarFile())
				.isSameAs(nestedJarFile);
	}

	@Test
	public void getNestedJarEntryUrl() throws Exception {
		JarFile nestedJarFile = this.jarFile
				.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
		URL url = nestedJarFile.getJarEntry("3.dat").getUrl();
		assertThat(url.toString())
				.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat");
		InputStream inputStream = url.openStream();
		assertThat(inputStream).isNotNull();
		assertThat(inputStream.read()).isEqualTo(3);
	}

	@Test
	public void createUrlFromString() throws Exception {
		JarFile.registerUrlProtocolHandler();
		String spec = "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat";
		URL url = new URL(spec);
		assertThat(url.toString()).isEqualTo(spec);
		InputStream inputStream = url.openStream();
		assertThat(inputStream).isNotNull();
		assertThat(inputStream.read()).isEqualTo(3);
		JarURLConnection connection = (JarURLConnection) url.openConnection();
		assertThat(connection.getURL().toString()).isEqualTo(spec);
		assertThat(connection.getJarFileURL().toString())
				.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
		assertThat(connection.getEntryName()).isEqualTo("3.dat");
	}

	@Test
	public void createNonNestedUrlFromString() throws Exception {
		nonNestedJarFileFromString("jar:" + this.rootJarFile.toURI() + "!/2.dat");
	}

	@Test
	public void createNonNestedUrlFromPathString() throws Exception {
		nonNestedJarFileFromString(
				"jar:" + this.rootJarFile.toPath().toUri() + "!/2.dat");
	}

	private void nonNestedJarFileFromString(String spec) throws Exception {
		JarFile.registerUrlProtocolHandler();
		URL url = new URL(spec);
		assertThat(url.toString()).isEqualTo(spec);
		InputStream inputStream = url.openStream();
		assertThat(inputStream).isNotNull();
		assertThat(inputStream.read()).isEqualTo(2);
		JarURLConnection connection = (JarURLConnection) url.openConnection();
		assertThat(connection.getURL().toString()).isEqualTo(spec);
		assertThat(connection.getJarFileURL().toURI())
				.isEqualTo(this.rootJarFile.toURI());
		assertThat(connection.getEntryName()).isEqualTo("2.dat");
	}

	@Test
	public void getDirectoryInputStream() throws Exception {
		InputStream inputStream = this.jarFile
				.getInputStream(this.jarFile.getEntry("d/"));
		assertThat(inputStream).isNotNull();
		assertThat(inputStream.read()).isEqualTo(-1);
	}

	@Test
	public void getDirectoryInputStreamWithoutSlash() throws Exception {
		InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d"));
		assertThat(inputStream).isNotNull();
		assertThat(inputStream.read()).isEqualTo(-1);
	}

	@Test
	public void sensibleToString() throws Exception {
		assertThat(this.jarFile.toString()).isEqualTo(this.rootJarFile.getPath());
		assertThat(this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))
				.toString()).isEqualTo(this.rootJarFile.getPath() + "!/nested.jar");
	}

	@Test
	public void verifySignedJar() throws Exception {
		String classpath = System.getProperty("java.class.path");
		String[] entries = classpath.split(System.getProperty("path.separator"));
		String signedJarFile = null;
		for (String entry : entries) {
			if (entry.contains("bcprov")) {
				signedJarFile = entry;
			}
		}
		assertThat(signedJarFile).isNotNull();
		java.util.jar.JarFile jarFile = new JarFile(new File(signedJarFile));
		jarFile.getManifest();
		Enumeration<JarEntry> jarEntries = jarFile.entries();
		while (jarEntries.hasMoreElements()) {
			JarEntry jarEntry = jarEntries.nextElement();
			InputStream inputStream = jarFile.getInputStream(jarEntry);
			inputStream.skip(Long.MAX_VALUE);
			inputStream.close();
			if (!jarEntry.getName().startsWith("META-INF") && !jarEntry.isDirectory()
					&& !jarEntry.getName().endsWith("TigerDigest.class")) {
				assertThat(jarEntry.getCertificates()).isNotNull();
			}
		}
		jarFile.close();
	}

	@Test
	public void jarFileWithScriptAtTheStart() throws Exception {
		File file = this.temporaryFolder.newFile();
		InputStream sourceJarContent = new FileInputStream(this.rootJarFile);
		FileOutputStream outputStream = new FileOutputStream(file);
		StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream);
		FileCopyUtils.copy(sourceJarContent, outputStream);
		this.rootJarFile = file;
		this.jarFile = new JarFile(file);
		// Call some other tests to verify
		getEntries();
		getNestedJarFile();
	}

	@Test
	public void cannotLoadMissingJar() throws Exception {
		// relates to gh-1070
		JarFile nestedJarFile = this.jarFile
				.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
		URL nestedUrl = nestedJarFile.getUrl();
		URL url = new URL(nestedUrl, nestedJarFile.getUrl() + "missing.jar!/3.dat");
		this.thrown.expect(FileNotFoundException.class);
		url.openConnection().getInputStream();
	}

	@Test
	public void registerUrlProtocolHandlerWithNoExistingRegistration() {
		String original = System.getProperty(PROTOCOL_HANDLER);
		try {
			System.clearProperty(PROTOCOL_HANDLER);
			JarFile.registerUrlProtocolHandler();
			String protocolHandler = System.getProperty(PROTOCOL_HANDLER);
			assertThat(protocolHandler).isEqualTo(HANDLERS_PACKAGE);
		}
		finally {
			if (original == null) {
				System.clearProperty(PROTOCOL_HANDLER);
			}
			else {
				System.setProperty(PROTOCOL_HANDLER, original);
			}
		}
	}

	@Test
	public void registerUrlProtocolHandlerAddsToExistingRegistration() {
		String original = System.getProperty(PROTOCOL_HANDLER);
		try {
			System.setProperty(PROTOCOL_HANDLER, "com.example");
			JarFile.registerUrlProtocolHandler();
			String protocolHandler = System.getProperty(PROTOCOL_HANDLER);
			assertThat(protocolHandler).isEqualTo("com.example|" + HANDLERS_PACKAGE);
		}
		finally {
			if (original == null) {
				System.clearProperty(PROTOCOL_HANDLER);
			}
			else {
				System.setProperty(PROTOCOL_HANDLER, original);
			}
		}
	}

}
