package com.esotericsoftware.scar;

import static com.esotericsoftware.minlog.Log.*;
import static com.esotericsoftware.scar.Jar.*;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.Writer;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import org.apache.commons.net.ftp.FTPClient;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;

import com.esotericsoftware.wildcard.Paths;

import SevenZip.LzmaAlone;

// BOZO - Add javadocs method.

/** Provides utility methods for common Java build tasks. */
public class Scar {
	/** The Scar installation directory. The value comes from the SCAR_HOME environment variable, if it exists. Alternatively, the
	 * "scar.home" System property can be defined. */
	static public final String SCAR_HOME;

	static {
		if (System.getProperty("scar.home") != null)
			SCAR_HOME = System.getProperty("scar.home");
		else
			SCAR_HOME = System.getenv("SCAR_HOME");
	}

	/** The command line arguments Scar was started with. Empty if Scar was started with no arguments or Scar was not started from
	 * the command line. */
	static public Arguments args = new Arguments();

	/** The Java installation directory. */
	static public final String JAVA_HOME = System.getProperty("java.home");

	/** True if running on a Mac OS. */
	static public final boolean isMac = System.getProperty("os.name").toLowerCase().contains("mac os x");

	/** True if running on a Windows OS. */
	static public final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");

	static {
		Paths.setDefaultGlobExcludes("**/.svn/**");
	}

	/** Returns the full path for the specified file name in the current working directory, the {@link #SCAR_HOME}, and the bin
	 * directory of {@link #JAVA_HOME}. */
	static public String resolvePath (String fileName) {
		if (fileName == null) return null;

		String foundFile;
		while (true) {
			foundFile = canonical(fileName);
			if (fileExists(foundFile)) break;

			foundFile = new File(SCAR_HOME, fileName).getPath();
			if (fileExists(foundFile)) break;

			foundFile = new File(JAVA_HOME, "bin/" + fileName).getPath();
			if (fileExists(foundFile)) break;

			foundFile = fileName;
			break;
		}
		if (TRACE) trace("scar", "Path \"" + fileName + "\" resolved to: " + foundFile);
		return foundFile;
	}

	/** Encodes the specified file with GZIP. The resulting filename is the filename plus ".gz". The file is deleted after
	 * encoding.
	 * @return The path to the encoded file. */
	static public String gzip (String file) throws IOException {
		String gzipFile = gzip(file, file + ".gz");
		delete(file);
		return gzipFile;
	}

	/** Encodes the specified file with GZIP.
	 * @return The path to the encoded file. */
	static public String gzip (String file, String gzipFile) throws IOException {
		if (file == null) throw new IllegalArgumentException("file cannot be null.");
		if (gzipFile == null) throw new IllegalArgumentException("gzipFile cannot be null.");

		if (DEBUG) debug("scar", "GZIP encoding: " + file + " -> " + gzipFile);

		GZIPOutputStream output = new GZIPOutputStream(new FileOutputStream(gzipFile));
		try {
			copyStream(new FileInputStream(file), output);
		} finally {
			try {
				output.close();
			} catch (Exception ignored) {
			}
		}
		return gzipFile;
	}

	/** Decodes the specified GZIP file. The filename must end in ".gz" and the resulting filename has this stripped. The encoded
	 * file is deleted after decoding.
	 * @return The path to the decoded file. */
	static public String ungzip (String gzipFile) throws IOException {
		if (gzipFile == null) throw new IllegalArgumentException("gzipFile cannot be null.");
		if (!gzipFile.endsWith(".gz")) throw new IllegalArgumentException("gzipFile must end with .gz: " + gzipFile);

		String file = ungzip(gzipFile, substring(gzipFile, 0, -3));
		delete(gzipFile);
		return file;
	}

	/** Decodes the specified GZIP file.
	 * @return The path to the decoded file. */
	static public String ungzip (String gzipFile, String file) throws IOException {
		if (gzipFile == null) throw new IllegalArgumentException("gzipFile cannot be null.");
		if (file == null) throw new IllegalArgumentException("file cannot be null.");

		if (DEBUG) debug("scar", "GZIP decoding: " + gzipFile + " -> " + file);

		FileOutputStream output = new FileOutputStream(file);
		try {
			copyStream(new GZIPInputStream(new FileInputStream(gzipFile)), output);
		} finally {
			try {
				output.close();
			} catch (Exception ignored) {
			}
		}
		return file;
	}

	/** Encodes the specified files with ZIP.
	 * @return The path to the encoded file. */
	static public String zip (Paths paths, String zipFile) throws IOException {
		if (paths == null) throw new IllegalArgumentException("paths cannot be null.");
		if (zipFile == null) throw new IllegalArgumentException("zipFile cannot be null.");

		if (DEBUG) debug("scar", "Creating ZIP (" + paths.count() + " entries): " + zipFile);

		paths.zip(zipFile);
		return zipFile;
	}

	/** Decodes the specified ZIP file.
	 * @return The path to the output directory. */
	static public String unzip (String zipFile, String outputDir) throws IOException {
		if (zipFile == null) throw new IllegalArgumentException("zipFile cannot be null.");
		if (outputDir == null) throw new IllegalArgumentException("outputDir cannot be null.");

		if (DEBUG) debug("scar", "ZIP decoding: " + zipFile + " -> " + outputDir);

		ZipInputStream input = new ZipInputStream(new FileInputStream(zipFile));
		try {
			byte[] buffer = new byte[1024 * 10];
			while (true) {
				ZipEntry entry = input.getNextEntry();
				if (entry == null) break;
				File file = new File(outputDir, entry.getName());
				if (entry.isDirectory()) {
					mkdir(file.getPath());
					continue;
				}
				mkdir(file.getParent());
				FileOutputStream output = new FileOutputStream(file);
				try {
					while (true) {
						int length = input.read(buffer);
						if (length == -1) break;
						output.write(buffer, 0, length);
					}
				} finally {
					try {
						output.close();
					} catch (Exception ignored) {
					}
				}
			}
		} finally {
			try {
				input.close();
			} catch (Exception ignored) {
			}
		}
		return outputDir;
	}

	/** Encodes the specified file with LZMA. The resulting filename is the filename plus ".lzma". The file is deleted after
	 * encoding.
	 * @return The path to the encoded file. */
	static public String lzma (String file) throws IOException {
		String lzmaFile = lzma(file, file + ".lzma");
		delete(file);
		return lzmaFile;
	}

	/** Encodes the specified file with LZMA.
	 * @return The path to the encoded file. */
	static public String lzma (String file, String lzmaFile) throws IOException {
		if (file == null) throw new IllegalArgumentException("file cannot be null.");
		if (lzmaFile == null) throw new IllegalArgumentException("lzmaFile cannot be null.");

		if (DEBUG) debug("scar", "LZMA encoding: " + file + " -> " + lzmaFile);

		try {
			LzmaAlone.main(new String[] {"e", file, lzmaFile});
		} catch (Exception ex) {
			throw new IOException("Error lzma compressing file: " + file, ex);
		}
		return lzmaFile;
	}

	/** Decodes the specified LZMA file. The filename must end in ".lzma" and the resulting filename has this stripped. The encoded
	 * file is deleted after decoding.
	 * @return The path to the decoded file. */
	static public String unlzma (String lzmaFile) throws IOException {
		if (lzmaFile == null) throw new IllegalArgumentException("lzmaFile cannot be null.");
		if (!lzmaFile.endsWith(".lzma")) throw new IllegalArgumentException("lzmaFile must end with .lzma: " + lzmaFile);

		String file = unlzma(lzmaFile, substring(lzmaFile, 0, -5));
		delete(lzmaFile);
		return file;
	}

	/** Decodes the specified LZMA file.
	 * @return The path to the decoded file. */
	static public String unlzma (String lzmaFile, String file) throws IOException {
		if (lzmaFile == null) throw new IllegalArgumentException("lzmaFile cannot be null.");
		if (file == null) throw new IllegalArgumentException("file cannot be null.");

		if (DEBUG) debug("scar", "LZMA decoding: " + lzmaFile + " -> " + file);

		try {
			LzmaAlone.main(new String[] {"d", lzmaFile, file});
		} catch (Exception ex) {
			throw new IOException("Error lzma decompressing file: " + file, ex);
		}
		return file;
	}

	static public String tempFile (String prefix) throws IOException {
		return File.createTempFile(prefix, null).getAbsolutePath();
	}

	static public String tempDirectory (String prefix) throws IOException {
		File file = File.createTempFile(prefix, null);
		if (!file.delete()) throw new IOException("Unable to delete temp file: " + file);
		if (!file.mkdir()) throw new IOException("Unable to create temp directory: " + file);
		return file.getAbsolutePath();
	}

	static public String shell (String command) throws IOException {
		return shell(null, command);
	}

	/** Splits the specified command at spaces that are not surrounded by quotes and passes the result to
	 * {@link #shell(Map, String...)}. */
	static public String shell (Map<? extends String, ? extends String> env, String command) throws IOException {
		List<String> matchList = new ArrayList<String>();
		Pattern regex = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'");
		Matcher regexMatcher = regex.matcher(command);
		while (regexMatcher.find()) {
			if (regexMatcher.group(1) != null)
				matchList.add(regexMatcher.group(1));
			else if (regexMatcher.group(2) != null)
				matchList.add(regexMatcher.group(2));
			else
				matchList.add(regexMatcher.group());
		}
		return shell(env, matchList.toArray(new String[matchList.size()]));
	}

	static public String shell (String... command) throws IOException {
		return shell(null, command);
	}

	/** Executes the specified shell command with the specified environment variables. {@link #resolvePath(String)} is used to
	 * locate the file to execute. If not found, on Windows the same filename with an "exe" extension is also tried.
	 * @param env May be null. */
	static public String shell (Map<? extends String, ? extends String> env, String... command) throws IOException {
		if (command == null) throw new IllegalArgumentException("command cannot be null.");
		if (command.length == 0) throw new IllegalArgumentException("command cannot be empty.");
		if (isWindows && command.length > 8192)
			throw new IllegalArgumentException("command is too long: " + command.length + " > 8192");

		String originalCommand = command[0];
		command[0] = resolvePath(command[0]);
		if (!fileExists(command[0]) && isWindows) {
			command[0] = resolvePath(command[0] + ".exe");
			if (!fileExists(command[0])) command[0] = originalCommand;
		}

		if (TRACE) {
			StringBuilder buffer = new StringBuilder(256);
			for (String text : command) {
				buffer.append(text);
				buffer.append(' ');
			}
			trace("scar", "Executing command: " + buffer);
		}
		ProcessBuilder builder = new ProcessBuilder(command).redirectErrorStream(true);
		if (env != null) {
			builder.environment().putAll(env);
		}
		final Process process = builder.start();
		final StringBuilder outputBuffer = new StringBuilder(512);
		new Thread("shell") {
			public void run () {
				BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
				try {
					while (true) {
						String line = reader.readLine();
						if (line == null) break;
						if (INFO && (line.length() > 0 || outputBuffer.length() > 0)) info("scar", line);
						outputBuffer.append(line);
						outputBuffer.append('\n');
					}
					reader.close();
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		}.start();

		try {
			process.waitFor();
		} catch (InterruptedException ignored) {
		}
		if (process.exitValue() != 0) {
			StringBuilder buffer = new StringBuilder(256);
			for (String text : command) {
				buffer.append(text);
				buffer.append(' ');
			}
			throw new RuntimeException("Error executing command: " + buffer);
		}
		return outputBuffer.toString();
	}

	/** Reads to the end of the input stream and writes the bytes to the output stream. The input stream is closed, the output is
	 * not. */
	static public void copyStream (InputStream input, OutputStream output) throws IOException {
		if (input == null) throw new IllegalArgumentException("input cannot be null.");
		if (output == null) throw new IllegalArgumentException("output cannot be null.");

		try {
			byte[] buffer = new byte[4096];
			while (true) {
				int length = input.read(buffer);
				if (length == -1) break;
				output.write(buffer, 0, length);
			}
		} finally {
			try {
				input.close();
			} catch (Exception ignored) {
			}
		}
	}

	/** Copies a file, overwriting any existing file at the destination. */
	static public String copyFile (String in, String out) throws IOException {
		if (in == null) throw new IllegalArgumentException("in cannot be null.");
		if (out == null) throw new IllegalArgumentException("out cannot be null.");

		if (TRACE) trace("scar", "Copying file: " + in + " -> " + out);

		FileChannel sourceChannel = null;
		FileChannel destinationChannel = null;
		try {
			sourceChannel = new FileInputStream(in).getChannel();
			destinationChannel = new FileOutputStream(out).getChannel();
			sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
		} catch (IOException ex) {
			throw new IOException("Error copying: " + in + "\nTo: " + out, ex);
		} finally {
			try {
				if (sourceChannel != null) sourceChannel.close();
			} catch (Exception ignored) {
			}
			try {
				if (destinationChannel != null) destinationChannel.close();
			} catch (Exception ignored) {
			}
		}
		return out;
	}

	/** Moves a file, overwriting any existing file at the destination. */
	static public String moveFile (String in, String out) throws IOException {
		if (in == null) throw new IllegalArgumentException("in cannot be null.");
		if (out == null) throw new IllegalArgumentException("out cannot be null.");

		copyFile(in, out);
		delete(in);
		return out;
	}

	static public byte[] readBytes (String file) throws IOException {
		long fileSize = fileSize(file);
		ByteArrayOutputStream output = new ByteArrayOutputStream(fileSize == 0 ? 1024 : (int)fileSize);
		FileInputStream input = new FileInputStream(file);
		byte[] buffer = new byte[256];
		while (true) {
			int length = input.read(buffer);
			if (length == -1) break;
			output.write(buffer, 0, length);
		}
		return output.toByteArray();
	}

	static public String readString (String file) throws IOException {
		return readString(file, null);
	}

	static public String readString (String fileName, String charset) throws IOException {
		if (fileName == null) throw new IllegalArgumentException("file cannot be null.");
		File file = new File(fileName);
		int fileLength = (int)file.length();
		if (fileLength == 0) fileLength = 512;
		StringBuilder output = new StringBuilder(fileLength);
		InputStreamReader reader = null;
		try {
			if (charset == null)
				reader = new InputStreamReader(new FileInputStream(file));
			else
				reader = new InputStreamReader(new FileInputStream(file), charset);
			char[] buffer = new char[256];
			while (true) {
				int length = reader.read(buffer);
				if (length == -1) break;
				output.append(buffer, 0, length);
			}
		} catch (IOException ex) {
			throw new RuntimeException("Error reading layout file: " + fileName, ex);
		} finally {
			try {
				if (reader != null) reader.close();
			} catch (IOException ignored) {
			}
		}
		return output.toString();
	}

	static public String writeFile (String fileName, String contents, boolean append) {
		return writeFile(fileName, contents, append, null);
	}

	static public String writeFile (String fileName, String contents, boolean append, String charset) {
		Writer writer = null;
		try {
			File file = new File(fileName);
			FileOutputStream output = new FileOutputStream(file, append);
			if (charset == null)
				writer = new OutputStreamWriter(output);
			else
				writer = new OutputStreamWriter(output, charset);
			writer.write(contents);
			return file.getAbsolutePath();
		} catch (Exception ex) {
			throw new RuntimeException("Error writing file: " + fileName, ex);
		} finally {
			try {
				if (writer != null) writer.close();
			} catch (Exception ignored) {
			}
		}
	}

	/** Deletes a file or directory and all files and subdirecties under it. */
	static public boolean delete (String fileName) {
		if (fileName == null) throw new IllegalArgumentException("fileName cannot be null.");

		File file = new File(fileName);
		if (file.exists() && file.isDirectory()) {
			File[] files = file.listFiles();
			for (int i = 0, n = files.length; i < n; i++) {
				if (files[i].isDirectory())
					delete(files[i].getAbsolutePath());
				else {
					if (TRACE) trace("scar", "Deleting file: " + files[i]);
					files[i].delete();
				}
			}
		}
		if (TRACE) trace("scar", "Deleting file: " + file);
		return file.delete();
	}

	/** Creates the directories in the specified path. */
	static public String mkdir (String path) {
		if (path == null) throw new IllegalArgumentException("path cannot be null.");

		if (new File(path).mkdirs() && TRACE) trace("scar", "Created directory: " + path);
		return path;
	}

	/** Returns true if the file exists. */
	static public boolean fileExists (String path) {
		if (path == null) throw new IllegalArgumentException("path cannot be null.");

		return new File(path).exists();
	}

	static public long fileSize (String path) {
		if (path == null) throw new IllegalArgumentException("path cannot be null.");

		return new File(path).length();
	}

	/** Returns the canonical path for the specified path. Eg, if "." is passed, this will resolve the actual path and return
	 * it. */
	static public String canonical (String path) {
		if (path == null) throw new IllegalArgumentException("path cannot be null.");

		File file = new File(path);
		try {
			return file.getCanonicalPath();
		} catch (IOException ex) {
			file = file.getAbsoluteFile();
			if (file.getName().equals(".")) file = file.getParentFile();
			return file.getPath();
		}
	}

	/** Returns only the filename portion of the specified path. */
	static public String fileName (String path) {
		return new File(canonical(path)).getName();
	}

	/** Returns only the filename portion of the specified path. */
	static public long fileLastModified (String path) {
		return new File(canonical(path)).lastModified();
	}

	/** Returns the parent directory of the specified path. */
	static public String parent (String path) {
		return new File(canonical(path)).getParent();
	}

	/** Returns only the extension portion of the specified path, or an empty string if there is no extension. */
	static public String fileExtension (String file) {
		if (file == null) throw new IllegalArgumentException("fileName cannot be null.");

		int commaIndex = file.indexOf('.');
		if (commaIndex == -1) return "";
		return file.substring(commaIndex + 1);
	}

	/** Returns only the filename portion of the specified path, without the extension, if any. */
	static public String fileWithoutExtension (String file) {
		if (file == null) throw new IllegalArgumentException("fileName cannot be null.");

		int commaIndex = file.indexOf('.');
		if (commaIndex == -1) commaIndex = file.length();
		int slashIndex = file.replace('\\', '/').lastIndexOf('/');
		if (slashIndex == -1)
			slashIndex = 0;
		else
			slashIndex++;
		return file.substring(slashIndex, commaIndex);
	}

	/** Shortcut for System.out.println. */
	static public void log (String text) {
		System.out.println(text);
	}

	/** Returns a substring of the specified text.
	 * @param end The end index of the substring. If negative, the index used will be "text.length() + end". */
	static public String substring (String text, int start, int end) {
		if (text == null) throw new IllegalArgumentException("text cannot be null.");

		if (end >= 0) return text.substring(start, end);
		return text.substring(start, text.length() + end);
	}

	static public void jws (String inputDir, String outputDir, boolean pack, String keystoreFile, String alias, String password)
		throws IOException {
		if (inputDir == null) throw new IllegalArgumentException("inputDir cannot be null.");
		if (outputDir == null) throw new IllegalArgumentException("outputDir cannot be null.");
		if (keystoreFile == null) throw new IllegalArgumentException("keystoreFile cannot be null.");
		if (alias == null) throw new IllegalArgumentException("alias cannot be null.");
		if (password == null) throw new IllegalArgumentException("password cannot be null.");
		if (password.length() < 6) throw new IllegalArgumentException("password must be 6 or more characters.");

		mkdir(outputDir);
		paths(inputDir, "*.jar", "*.jnlp").copyTo(outputDir);
		for (String file : paths(outputDir, "*.jar"))
			sign(unpack200(pack200(unsign(file))), keystoreFile, alias, password);
		if (!pack) return;
		for (String file : paths(outputDir, "*.jar", "!*native*"))
			gzip(pack200(file));
	}

	static public void jwsHtaccess (String jwsDir) throws IOException {
		for (String packedFile : paths(jwsDir + "packed", "*.jar.pack.gz")) {
			String packedFileName = fileName(packedFile);
			String jarFileName = substring(packedFileName, 0, -8);
			FileWriter writer = new FileWriter(jwsDir + jarFileName + ".var");
			try {
				writer.write("URI: packed/" + packedFileName + "\n");
				writer.write("Content-Type: x-java-archive\n");
				writer.write("Content-Encoding: pack200-gzip\n");
				writer.write("URI: unpacked/" + jarFileName + "\n");
				writer.write("Content-Type: x-java-archive\n");
			} finally {
				try {
					writer.close();
				} catch (Exception ignored) {
				}
			}
		}
		FileWriter writer = new FileWriter(jwsDir + ".htaccess");
		try {
			writer.write("AddType application/x-java-jnlp-file .jnlp"); // JNLP mime type.
			writer.write("AddType application/x-java-archive .jar\n"); // JAR mime type.
			writer.write("AddHandler application/x-type-map .var\n"); // Enable type maps.
			writer.write("Options +MultiViews\n");
			writer.write("MultiViewsMatch Any\n"); // Apache 2.0 only.
			writer.write("<Files *.pack.gz>\n");
			writer.write("AddEncoding pack200-gzip .jar\n"); // Enable Content-Encoding header for .jar.pack.gz files.
			writer.write("RemoveEncoding .gz\n"); // Prevent mod_gzip from messing with the Content-Encoding response.
			writer.write("</Files>\n");
		} finally {
			try {
				writer.close();
			} catch (Exception ignored) {
			}
		}
	}

	static public void jnlp (String inputDir, String mainClass, String mainClassJar, String url, String company, String title,
		String splashImage) throws IOException {
		if (mainClass == null) throw new IllegalArgumentException("mainClass cannot be null.");
		if (url == null) throw new IllegalArgumentException("url cannot be null.");
		if (title == null) throw new IllegalArgumentException("title cannot be null.");
		if (company == null) throw new IllegalArgumentException("company cannot be null.");

		int firstSlash = url.indexOf("/", 7);
		int lastSlash = url.lastIndexOf("/");
		if (firstSlash == -1 || lastSlash == -1) throw new RuntimeException("Invalid url: " + url);
		String domain = url.substring(0, firstSlash + 1);
		String path = url.substring(firstSlash + 1, lastSlash + 1);
		String jnlpFile = url.substring(lastSlash + 1);

		FileWriter writer = new FileWriter(inputDir + jnlpFile);
		try {
			writer.write("<?xml version='1.0' encoding='utf-8'?>\n");
			writer.write("<jnlp spec='1.0+' codebase='" + domain + "' href='" + path + jnlpFile + "'>\n");
			writer.write("<information>\n");
			writer.write("\t<title>" + title + "</title>\n");
			writer.write("\t<vendor>" + company + "</vendor>\n");
			writer.write("\t<homepage href='" + domain + "'/>\n");
			writer.write("\t<description>" + title + "</description>\n");
			writer.write("\t<description kind='short'>" + title + "</description>\n");
			if (splashImage != null) writer.write("\t<icon kind='splash' href='" + path + splashImage + "'/>\n");
			writer.write("</information>\n");
			writer.write("<security>\n");
			writer.write("\t<all-permissions/>\n");
			writer.write("</security>\n");
			writer.write("<resources>\n");
			writer.write("\t<j2se href='http://java.sun.com/products/autodl/j2se' version='1.5+' max-heap-size='128m'/>\n");

			// JAR with main class first.
			writer.write("\t<jar href='" + path + mainClassJar + "'/>\n");

			// Rest of JARs, except natives.
			for (String file : paths(inputDir, "**/*.jar", "**/*.jar.pack.lzma", "!*native*", "!**/" + mainClassJar))
				writer.write("\t<jar href='" + path + fileName(file) + "'/>\n");

			writer.write("</resources>\n");
			Paths nativePaths = paths(inputDir, "*native*win*", "*win*native*");
			if (nativePaths.count() == 1) {
				writer.write("<resources os='Windows'>\n");
				writer.write("\t<j2se href='http://java.sun.com/products/autodl/j2se' version='1.5+' max-heap-size='128m'/>\n");
				writer.write("\t<nativelib href='" + path + nativePaths.getNames().get(0) + "'/>\n");
				writer.write("</resources>\n");
			}
			nativePaths = paths(inputDir, "*native*mac*", "*mac*native*");
			if (nativePaths.count() == 1) {
				writer.write("<resources os='Mac'>\n");
				writer.write("\t<j2se href='http://java.sun.com/products/autodl/j2se' version='1.5+' max-heap-size='128m'/>\n");
				writer.write("\t<nativelib href='" + path + nativePaths.getNames().get(0) + "'/>\n");
				writer.write("</resources>\n");
			}
			nativePaths = paths(inputDir, "*native*linux*", "*linux*native*");
			if (nativePaths.count() == 1) {
				writer.write("<resources os='Linux'>\n");
				writer.write("\t<j2se href='http://java.sun.com/products/autodl/j2se' version='1.5+' max-heap-size='128m'/>\n");
				writer.write("\t<nativelib href='" + path + nativePaths.getNames().get(0) + "'/>\n");
				writer.write("</resources>\n");
			}
			nativePaths = paths(inputDir, "*native*solaris*", "*solaris*native*");
			if (nativePaths.count() == 1) {
				writer.write("<resources os='SunOS'>\n");
				writer.write("\t<j2se href='http://java.sun.com/products/autodl/j2se' version='1.5+' max-heap-size='128m'/>\n");
				writer.write("\t<nativelib href='" + path + nativePaths.getNames().get(0) + "'/>\n");
				writer.write("</resources>\n");
			}
			writer.write("<application-desc main-class='" + mainClass + "'/>\n");
			writer.write("</jnlp>");
		} finally {
			try {
				writer.close();
			} catch (Exception ignored) {
			}
		}
	}

	static public void lwjglApplet (String inputDir, String outputDir, String keystoreFile, String alias, String password)
		throws IOException {
		if (inputDir == null) throw new IllegalArgumentException("inputDir cannot be null.");
		if (outputDir == null) throw new IllegalArgumentException("outputDir cannot be null.");
		if (keystoreFile == null) throw new IllegalArgumentException("keystoreFile cannot be null.");
		if (alias == null) throw new IllegalArgumentException("alias cannot be null.");
		if (password == null) throw new IllegalArgumentException("password cannot be null.");
		if (password.length() < 6) throw new IllegalArgumentException("password must be 6 or more characters.");

		mkdir(outputDir);
		paths(inputDir, "**/*.jar", "*.html", "*.htm").flatten().copyTo(outputDir);
		for (String jarFile : paths(outputDir, "*.jar")) {
			sign(unpack200(pack200(unsign(jarFile))), keystoreFile, alias, password);
			String fileName = fileName(jarFile);
			if (fileName.equals("lwjgl_util_applet.jar") || fileName.equals("lzma.jar")) continue;
			if (fileName.contains("native"))
				lzma(jarFile);
			else
				lzma(pack200(jarFile));
		}
	}

	static public void lwjglAppletHtml (String inputDir, String mainClass) throws IOException {
		if (INFO) info("scar", "Generating: applet.html");
		FileWriter writer = new FileWriter(inputDir + "applet.html");
		try {
			writer.write("<html>\n");
			writer.write("<head><title>Applet</title></head>\n");
			writer.write("<body>\n");
			writer.write(
				"<applet code='org.lwjgl.util.applet.AppletLoader' archive='lwjgl_util_applet.jar, lzma.jar' codebase='.' width='640' height='480'>\n");
			writer.write("<param name='al_version' value='1.0'>\n");
			writer.write("<param name='al_title' value='applet'>\n");
			writer.write("<param name='al_main' value='" + mainClass + "'>\n");
			writer.write("<param name='al_jars' value='");
			int i = 0;
			HashSet<String> names = new HashSet();
			for (String name : paths(inputDir, "*.jar.pack.lzma").getNames()) {
				names.add(name);
				if (i++ > 0) writer.write(", ");
				writer.write(name);
			}
			for (String name : paths(inputDir, "*.jar").getNames()) {
				if (names.contains(name + ".pack.lzma")) continue;
				if (i++ > 0) writer.write(", ");
				writer.write(name + ".pack.lzma");
			}
			writer.write("'>\n");
			Paths nativePaths = paths(inputDir, "*native*win*.jar.lzma", "*win*native*.jar.lzma");
			if (nativePaths.count() == 1) writer.write("<param name='al_windows' value='" + nativePaths.getNames().get(0) + "'>\n");
			nativePaths = paths(inputDir, "*native*mac*.jar.lzma", "*mac*native*.jar.lzma");
			if (nativePaths.count() == 1) writer.write("<param name='al_mac' value='" + nativePaths.getNames().get(0) + "'>\n");
			nativePaths = paths(inputDir, "*native*linux*.jar.lzma", "*linux*native*.jar.lzma");
			if (nativePaths.count() == 1) writer.write("<param name='al_linux' value='" + nativePaths.getNames().get(0) + "'>\n");
			nativePaths = paths(inputDir, "*native*solaris*.jar.lzma", "*solaris*native*.jar.lzma");
			if (nativePaths.count() == 1) writer.write("<param name='al_solaris' value='" + nativePaths.getNames().get(0) + "'>\n");
			writer.write("<param name='al_logo' value='appletlogo.png'>\n");
			writer.write("<param name='al_progressbar' value='appletprogress.gif'>\n");
			writer.write("<param name='separate_jvm' value='true'>\n");
			writer.write(
				"<param name='java_arguments' value='-Dsun.java2d.noddraw=true -Dsun.awt.noerasebackground=true -Dsun.java2d.d3d=false -Dsun.java2d.opengl=false -Dsun.java2d.pmoffscreen=false'>\n");
			writer.write("</applet>\n");
			writer.write("</body></html>\n");
		} finally {
			try {
				writer.close();
			} catch (Exception ignored) {
			}
		}
	}

	static public Paths path (String file) {
		return new Paths().addFile(file);
	}

	static public Paths path (String dir, String file) {
		return new Paths().add(dir, file);
	}

	static public Paths paths (String dir, String... patterns) {
		return new Paths(dir, patterns);
	}

	static public void compile (Paths source, Paths classpath, String outputDir, String targetVersion) {
		if (source.isEmpty()) {
			if (WARN) warn("scar", "No source files found.");
			return;
		}

		ArrayList<String> args = new ArrayList();
		if (TRACE) args.add("-verbose");
		args.add("-d");
		args.add(outputDir);
		args.add("-g:source,lines");
		args.add("-source");
		args.add(targetVersion);
		args.add("-target");
		args.add(targetVersion);
		args.add("-encoding");
		args.add("UTF-8");
// args.addAll(source.getPaths());
		if (classpath != null && !classpath.isEmpty()) {
			args.add("-classpath");
			args.add(isWindows ? classpath.toString(";") : classpath.toString(":"));
		}

		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		if (compiler == null)
			throw new RuntimeException("No compiler available. Ensure you are running from a 1.6+ JDK, and not a JRE.");
		DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
		StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
		Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromStrings(source.getPaths());
		JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, args, null, compilationUnits);

// JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		boolean s = task.call();
		try {
			fileManager.close();
		} catch (IOException ex) {
		}

// if (compiler.run(System.in, System.out, System.err, args.toArray(new String[args.size()])) != 0) {
		if (!s) {
			System.out.flush();
			System.err.flush();
			StringBuilder b = new StringBuilder();
			b.append("Compilation failed.\nSource: ").append(source.count()) //
				.append(" files\nClasspath:\n").append(classpath.toString("\n"));
			b.append("\nCompilation parameters: ");
			for (String a : args)
				b.append(a).append(' ');
			if (!diagnostics.getDiagnostics().isEmpty()) {
				Pattern pattern = Pattern.compile("(.*): error: (.*)", Pattern.DOTALL);
				boolean lastWasNote = false;
				for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
					Diagnostic.Kind kind = diagnostic.getKind();
					b.append(lastWasNote ? "\n" : "\n\n").append(diagnostic.getKind().name()).append(": ");
					lastWasNote = kind == Diagnostic.Kind.NOTE;

					String message = diagnostic.toString();
					if (message.toLowerCase().startsWith(diagnostic.getKind().name().toLowerCase() + ": "))
						message = message.substring(diagnostic.getKind().name().length() + 2);

					Matcher matcher = pattern.matcher(message);
					if (matcher.matches()) {
						String file = matcher.group(1);
						int slash = file.lastIndexOf('\\');
						if (slash == -1) slash = file.lastIndexOf('/');
						if (slash != -1) b.append(file.substring(slash + 1).replaceAll("\\.java(:\\d+)$", "$1")).append(": ");
						b.append(matcher.group(2)).append("\n  file: ").append(file);
					} else
						b.append(message);
				}
			}
			throw new RuntimeException(b.toString());
		}
		try {
			Thread.sleep(100);
		} catch (InterruptedException ex) {
		}
	}

	static public void executeCode (String code, HashMap<String, Object> parameters) {
		executeCode(code, parameters);
	}

	/** Compiles and executes the speFcified Java code. The code is compiled as if it were a Java method body.
	 * <p>
	 * Imports statements can be used before any code. These imports are automatically used:<br>
	 * import com.esotericsoftware.scar.Scar;<br>
	 * import com.esotericsoftware.wildcard.Paths;<br>
	 * import com.esotericsoftware.minlog.Log;<br>
	 * import static com.esotericsoftware.scar.Scar.*;<br>
	 * import static com.esotericsoftware.minlog.Log.*;<br>
	 * <p>
	 * Entries can be added to the classpath by using "classpath [url];" statements at the start of the code. These classpath
	 * entries are checked before the classloader that loaded the Scar class is checked. Examples:<br>
	 * classpath someTools.jar;<br>
	 * classpath some/directory/of/class/files;<br>
	 * classpath http://example.com/someTools.jar;<br>
	 * If a project parameter is not null, non-absolute classpath entries will be relative to the project directory.
	 * @param parameters These parameters will be available in the scope where the code is executed. */
	static public void executeCode (String code, HashMap<String, Object> parameters, Project project) {
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		if (compiler == null)
			throw new RuntimeException("No compiler available. Ensure you are running from a 1.6+ JDK, and not a JRE.");

		try {
			// Wrap code in a class.
			StringBuilder classBuffer = new StringBuilder(2048);
			classBuffer.append("import com.esotericsoftware.scar.*;\n");
			classBuffer.append("import com.esotericsoftware.minlog.Log;\n");
			classBuffer.append("import com.esotericsoftware.wildcard.Paths;\n");
			classBuffer.append("import static com.esotericsoftware.scar.Scar.*;\n");
			classBuffer.append("import static com.esotericsoftware.minlog.Log.*;\n");
			classBuffer.append("public class Generated {\n");
			int templateStartLines = 6;
			classBuffer.append("public void execute (");
			int i = 0;
			for (Entry<String, Object> entry : parameters.entrySet()) {
				if (i++ > 0) classBuffer.append(',');
				classBuffer.append('\n');
				templateStartLines++;
				classBuffer.append(entry.getValue().getClass().getName());
				classBuffer.append(' ');
				classBuffer.append(entry.getKey());
			}
			classBuffer.append("\n) throws Exception {\n");
			templateStartLines += 2;

			// Append code, collecting imports statements and classpath URLs.
			StringBuilder importBuffer = new StringBuilder(512);
			ArrayList<URL> classpathURLs = new ArrayList();
			BufferedReader reader = new BufferedReader(new StringReader(code));
			boolean header = true;
			while (true) {
				String line = reader.readLine();
				if (line == null) break;
				String trimmed = line.trim();
				if (header && trimmed.startsWith("import ") && trimmed.endsWith(";")) {
					importBuffer.append(line);
					importBuffer.append('\n');
				} else if (header && trimmed.startsWith("classpath ") && trimmed.endsWith(";")) {
					String path = substring(line.trim(), 10, -1);
					try {
						classpathURLs.add(new URL(path));
					} catch (MalformedURLException ex) {
						if (project != null) classpathURLs.add(new File(project.path(path)).toURI().toURL());
					}
				} else {
					if (trimmed.length() > 0) header = false;
					classBuffer.append(line);
					classBuffer.append('\n');
				}
			}
			classBuffer.append("}}");

			final String classCode = importBuffer.append(classBuffer).toString();
			if (TRACE) trace("scar", "Executing code:\n" + classCode);

			// Construct classpath option.
			List<String> options = new ArrayList<String>();
			{
				StringBuffer buffer = new StringBuffer(System.getProperty("java.class.path"));
				String pathSeparator = System.getProperty("path.separator");
				for (URL url : classpathURLs) {
					buffer.append(pathSeparator);
					buffer.append(new File(url.toURI()).getCanonicalPath());
				}
				if (TRACE) trace("scar", "Using classpath: " + buffer);
				options.add("-classpath");
				options.add(buffer.toString());
			}

			// Compile class.
			final ByteArrayOutputStream output = new ByteArrayOutputStream(32 * 1024);
			final SimpleJavaFileObject javaObject = new SimpleJavaFileObject(URI.create("Generated.java"),
				JavaFileObject.Kind.SOURCE) {
				public OutputStream openOutputStream () {
					return output;
				}

				public CharSequence getCharContent (boolean ignoreEncodingErrors) {
					return classCode;
				}
			};
			DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector();
			compiler.getTask(null, new ForwardingJavaFileManager(compiler.getStandardFileManager(null, null, null)) {
				public JavaFileObject getJavaFileForOutput (Location location, String className, JavaFileObject.Kind kind,
					FileObject sibling) {
					return javaObject;
				}
			}, diagnostics, options, null, Arrays.asList(new JavaFileObject[] {javaObject})).call();

			boolean error = false;
			for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
				if (diagnostic.getKind() == javax.tools.Diagnostic.Kind.ERROR) {
					error = true;
					break;
				}
			}
			if (error) {
				StringBuilder buffer = new StringBuilder(1024);
				for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
					if (buffer.length() > 0) buffer.append("\n");
					buffer.append("Line ");
					buffer.append(diagnostic.getLineNumber() - templateStartLines);
					buffer.append(": ");
					buffer.append(diagnostic.getMessage(null).replaceAll("^Generated.java:\\d+:\\d* ", ""));
				}
				throw new RuntimeException("Compilation errors:\n" + buffer);
			}

			// Load class.
			Class generatedClass = new URLClassLoader(classpathURLs.toArray(new URL[classpathURLs.size()]),
				Scar.class.getClassLoader()) {
				protected synchronized Class<?> loadClass (String name, boolean resolve) throws ClassNotFoundException {
					// Look in this classloader before the parent.
					Class c = findLoadedClass(name);
					if (c == null) {
						try {
							c = findClass(name);
						} catch (ClassNotFoundException e) {
							return super.loadClass(name, resolve);
						}
					}
					if (resolve) resolveClass(c);
					return c;
				}

				protected Class<?> findClass (String name) throws ClassNotFoundException {
					if (name.equals("Generated")) {
						byte[] bytes = output.toByteArray();
						return defineClass(name, bytes, 0, bytes.length);
					}
					return super.findClass(name);
				}
			}.loadClass("Generated");

			// Execute.
			Class[] parameterTypes = new Class[parameters.size()];
			Object[] parameterValues = new Object[parameters.size()];
			i = 0;
			for (Object object : parameters.values()) {
				parameterValues[i] = object;
				parameterTypes[i++] = object.getClass();
			}
			generatedClass.getMethod("execute", parameterTypes).invoke(generatedClass.newInstance(), parameterValues);
		} catch (Throwable ex) {
			throw new RuntimeException("Error executing code:\n" + code.trim(), ex);
		}
	}

	static public boolean ftpUpload (String server, String user, String password, String dir, Paths paths, boolean passive)
		throws IOException {
		FTPClient ftp = new FTPClient();
		InetAddress address = InetAddress.getByName(server);
		if (DEBUG) debug("scar", "Connecting to FTP server: " + address);
		ftp.connect(address);
		if (passive) ftp.enterLocalPassiveMode();
		if (!ftp.login(user, password)) {
			if (ERROR) error("scar", "FTP login failed for user: " + user);
			return false;
		}
		if (!ftp.changeWorkingDirectory(dir)) {
			if (ERROR) error("scar", "FTP directory change failed: " + dir);
			return false;
		}
		ftp.setFileType(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
		for (String path : paths) {
			if (INFO) info("scar", "FTP upload: " + path);
			BufferedInputStream input = new BufferedInputStream(new FileInputStream(path));
			try {
				ftp.storeFile(new File(path).getName(), input);
			} finally {
				try {
					input.close();
				} catch (Exception ignored) {
				}
			}
		}
		ftp.logout();
		ftp.disconnect();
		return true;
	}

	static public void sftpUpload (String server, int port, String user, String password, String dir, Paths paths)
		throws IOException {
		sftpUpload(server, port, user, password, dir, paths, null, true);
	}

	static public void sftpUpload (String server, int port, String user, String password, String dir, Paths paths,
		final ProgressMonitor monitor, final boolean printProgress) throws IOException {
		sftpUpload( //
			server, port, user, password, //
			null, 0, null, null, //
			dir, paths, null, true);
	}

	/** Upload through an SSH tunnel using an intermediate server.
	 * <p>
	 * Note some users have reported needing to use IP addresses. */
	static public void sftpUpload ( //
		String server1, int port1, String user1, final String password1, //
		String server2, int port2, String user2, final String password2, //
		String dir, Paths paths, final ProgressMonitor monitor, final boolean printProgress) throws IOException {

		Session session1 = null, session2 = null, session = null;
		try {
			long total = 0;
			for (String path : paths)
				total += new File(path).length();

			if (TRACE) {
				JSch.setLogger(new com.jcraft.jsch.Logger() {
					public boolean isEnabled (int pLevel) {
						return true;
					}

					public void log (int level, String message) {
						trace("scar", message);
					}
				});
			}

			JSch jsch = new JSch();
			ChannelSftp channel = null;
			boolean reconnecting = false, cd = true;
			for (String path : paths) {
				SftpFileUpload fileUpload = new SftpFileUpload(path, total, monitor, printProgress);
				if (INFO) info("scar", "SFTP upload: " + fileUpload.file.getName() + " -> " + dir);
				while (true) {
					// Connect.
					try {
						if (session1 == null || !session1.isConnected() || (server2 != null && !session2.isConnected())) {
							if (session1 != null) session1.disconnect();
							if (session2 != null) session2.disconnect();
							channel = null;

							session1 = jsch.getSession(user1, server1, port1);
							session1.setPassword(password1);
							session1.setConfig("StrictHostKeyChecking", "no");
							session1.connect(10000);

							if (server2 != null) {
								int forwardPort = session1.setPortForwardingL(0, server2, port2);
								session2 = jsch.getSession(user2, "127.0.0.1", forwardPort);
								session2.setPassword(password2);
								session2.setConfig("StrictHostKeyChecking", "no");
								session2.setHostKeyAlias(server2);
								session2.connect(10000);
								session = session2;
							} else
								session = session1;
						}
						if (channel == null || !channel.isConnected()) {
							channel = (ChannelSftp)session.openChannel("sftp");
							channel.connect(10000);
							reconnecting = false;
							cd = true;
						}
					} catch (Exception ex) {
						if (TRACE) trace("scar", "Connection error.", ex);
						if (!reconnecting) {
							reconnecting = true;
							if (WARN) warn("scar", "Connecting...");
						}
						try {
							Thread.sleep(250);
						} catch (Exception ignored) {
						}
						continue;
					}

					// Upload.
					try {
						if (cd) {
							fileUpload.cd(channel, dir);
							cd = false;
						}
						fileUpload.upload(channel);
						break;
					} catch (FileNotFoundException ex) {
						throw ex;
					} catch (Exception ex) {
						if (TRACE) trace("scar", "Error during upload.", ex);
						continue;
					}
				}
			}
		} catch (Exception ex) {
			throw new IOException(ex);
		} finally {
			if (session2 != null) session2.disconnect();
			if (session1 != null) session1.disconnect();
		}
	}

	static private class SftpFileUpload {
		final File file;
		final long total, fileLength, interval;
		long fileCount, lastCount, totalCount;
		final ProgressMonitor monitor;
		final boolean printProgress;

		final SftpProgressMonitor sftpMonitor = new SftpProgressMonitor() {
			public void init (int op, String source, String dest, long max) {
				if (!printProgress) return;
				System.out.print("|-");
				if (fileCount > 0) {
					lastCount = 0;
					while (fileCount - lastCount >= interval) {
						lastCount += interval;
						System.out.print("-");
					}
				}
			}

			public void end () {
				if (printProgress) System.out.println("|");
			}

			public boolean count (long c) {
				totalCount += c;
				fileCount += c;
				if (printProgress) {
					while (fileCount - lastCount >= interval) {
						lastCount += interval;
						System.out.print("-");
					}
				}
				if (monitor != null) monitor.progress((float)(fileCount / (double)fileLength), (float)(totalCount / (double)total));
				return true;
			}
		};

		SftpFileUpload (String path, long total, ProgressMonitor monitor, boolean printProgress) throws IOException {
			this.printProgress = printProgress;
			file = new File(path);
			this.total = total;
			this.monitor = monitor;
			fileLength = file.length();
			interval = Math.max(1, fileLength / 76);
		}

		void cd (ChannelSftp channel, String path) throws Exception {
			try {
				channel.cd(path);
			} catch (SftpException ex) {
				if (ex.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) throw new Exception("Error setting remote directory: " + path, ex);
				mkdirs(channel, path);
			}
		}

		private void mkdirs (ChannelSftp channel, String path) throws Exception {
			StringBuilder good = new StringBuilder(path.length());
			try {
				for (String dir : path.split("/")) {
					if (dir.isEmpty()) continue;
					if (good.length() == 0) dir = '/' + dir;
					good.append(dir);
					try {
						channel.cd(dir);
					} catch (SftpException ex) {
						if (ex.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) throw ex;
						channel.mkdir(dir);
						if (TRACE) trace("scar", "Created remote directory: " + good);
						channel.cd(dir);
					}
					good.append("/");
				}
			} catch (Exception ex) {
				throw new Exception("Error creating remote directory: " + good, ex);
			}
		}

		void upload (ChannelSftp channel) throws Exception {
			BufferedInputStream input = new BufferedInputStream(new FileInputStream(file));
			try {
				channel.put(input, file.getName(), sftpMonitor, fileCount == 0 ? ChannelSftp.OVERWRITE : ChannelSftp.RESUME);
			} finally {
				try {
					input.close();
				} catch (Exception ignored) {
				}
			}
		}
	}

	static public String ssh (String server, String user, String password, String command, boolean requireZeroExitCode)
		throws IOException {
		return ssh(server, 22, user, password, command, requireZeroExitCode);
	}

	static public String ssh (String server, int port, String user, String password, String command, boolean requireZeroExitCode)
		throws IOException {
		if (INFO) info("scar", "SSH: " + command);
		StringBuilder result = new StringBuilder();
		Session session = null;
		ChannelExec channel = null;

		// Connect.
		BufferedReader reader;
		while (true) {
			try {
				session = new JSch().getSession(user, server, port);
				session.setConfig("StrictHostKeyChecking", "no");
				session.setPassword(password);
				session.connect(10000);
				channel = (ChannelExec)session.openChannel("exec");
				channel.setCommand(command);
				channel.setInputStream(null);
				channel.setErrStream(System.err);
				reader = new BufferedReader(new InputStreamReader(channel.getInputStream()));
				channel.connect(10000);
				break;
			} catch (Exception ex) {
				if (TRACE) trace("scar", "Connection error.", ex);
				if (WARN) warn("scar", "Connecting...");
				try {
					Thread.sleep(250);
				} catch (Exception ignored) {
				}
				continue;
			}
		}

		try {
			byte[] buffer = new byte[1024];
			while (true) {
				while (true) {
					String line = reader.readLine();
					if (line == null) break;
					result.append(line);
					result.append('\n');
					if (INFO) info("scar", line);
				}
				if (channel.isClosed()) {
					if (INFO) info("scar", "Exit: " + channel.getExitStatus());
					if (requireZeroExitCode && channel.getExitStatus() != 0)
						throw new RuntimeException("Error executing command: " + command);
					break;
				}
				try {
					Thread.sleep(100);
				} catch (Exception ee) {
				}
			}
		} catch (Exception ex) {
			throw new IOException(ex);
		} finally {
			try {
				if (session != null) session.disconnect();
				if (channel != null) channel.disconnect();
			} catch (Exception ignored) {
			}
		}
		return result.toString();
	}

	static public String http (String url) throws IOException {
		if (INFO) info("scar", "HTTP: " + url);
		Scanner scanner = new Scanner(new URL(url).openStream()).useDelimiter("\\A");
		return scanner.hasNext() ? scanner.next() : "";
	}

	static public ArrayList list (Object... objects) {
		if (objects == null) throw new IllegalArgumentException("objects cannot be null.");
		ArrayList list = new ArrayList(objects.length);
		for (Object o : objects)
			list.add(o);
		return list;
	}

	private Scar () {
	}

	static public interface ProgressMonitor {
		public void progress (float fileProgress, float totalProgress);
	}

	static public void main (String[] args) throws IOException {
		Scar.args = new Arguments(args);

		if (Scar.args.has("trace"))
			TRACE();
		else if (Scar.args.has("debug"))
			DEBUG();
		else if (Scar.args.has("info"))
			INFO();
		else if (Scar.args.has("warn"))
			WARN();
		else if (Scar.args.has("error")) //
			ERROR();

		// BOZO - Add something that can execute scar methods!
	}
}