Everge.java

package bjc.everge;

import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.locks.*;
import java.util.regex.*;

/**
 * Everge front-end application.
 *
 * @author Ben Culkin
 */
public class Everge {
	/**
	 * Details how we handle our input.
	 */
	public static enum InputStatus {
		/**
		 * Process the input as a single string.
		 */
		ALL,
		/**
		 * Process the input line-by-line.
		 */
		LINE,
		/**
		 * Process the input, splitting it around occurances of a regex.
		 */
		REGEX;
	}

	// Options for doing repl-pairs
	private ReplOpts ropts = new ReplOpts();

	// Pair repository
	private ReplSet replSet = new ReplSet();

	// Input status
	private InputStatus inputStat = InputStatus.ALL;

	// Are we processing CLI args? (haven't seen a -- yet)
	private boolean doingArgs = true;

	// Should an NL be printed after each replace?
	private boolean printNL = true;

	// Verbosity level
	private int verbosity;

	// The pattern to use for REGEX input mode
	private String pattern;

	// The queue of arguments to process
	private Deque<String> argQue = new LinkedList<>();

	// Used to prevent inter-mixing argument alterations with input processing.
	private ReadWriteLock argLock = new ReentrantReadWriteLock();

	// Input/output streams
	/**
	 * Stream to use for normal output.
	 */
	private PrintStream outStream = System.out;
	/**
	 * Stream to use for error output.
	 */
	private LogStream errStream = new LogStream(System.err);

	/**
	 * Set the output stream.
	 * 
	 * @param out
	 *            The output stream..
	 */
	public void setOutput(PrintStream out) {
		outStream = out;
	}

	/**
	 * Set the output stream.
	 * 
	 * @param out
	 *            The output stream..
	 */
	public void setOutput(OutputStream out) {
		outStream = new PrintStream(out);
	}

	/**
	 * Set the error stream.
	 * 
	 * @param err
	 *            The error stream.
	 */
	public void setError(PrintStream err) {
		errStream = new LogStream(err);
	}

	/**
	 * Set the error stream.
	 * 
	 * @param err
	 *            The error stream.
	 */
	public void setError(OutputStream err) {
		errStream = new LogStream(new PrintStream(err));
	}

	/**
	 * Main method for front end,
	 *
	 * @param args
	 *             The CLI arguments.
	 */
	public static void main(String[] args) {
		Everge evg = new Everge();

		evg.processArgs(args);
	}

	/**
	 * Process one or more command line arguments.
	 *
	 * @param args
	 *             The arguments to process.
	 * @return Whether we processed succesfully or not.
	 */
	public boolean processArgs(String... args) {
		List<String> errs = new ArrayList<>();

		boolean stat = processArgs(errs, args);
		if (verbosity >= 2) {
			String argString = args.length > 0 ? "arguments" : "argument";

			errStream.infof("[INFO] Processed %d %s\n", args.length, argString);
			int argc = 0;
			if (verbosity >= 3) {
				String arg = args[argc++];
				errStream.tracef("[TRACE]\tArg %d: '%s\n", argc, arg);
			}
		}

		if (!stat) {
			for (String err : errs) {
				errStream.errorf("%s\n", err);
			}
		}

		return stat;
	}

	/**
	 * Process one or more command line arguments.
	 *
	 * @param args
	 *             The arguments to process.
	 * @param errs
	 *             The list to stash errors in.
	 * @return Whether we processed succesfully or not.
	 */
	public boolean processArgs(List<String> errs, String... args) {
		argLock.writeLock().lock();

		boolean retStat = true;

		try {
			loadQueue(args);

			// Process CLI args
			while (argQue.size() > 0) {
				String arg = argQue.pop();

				retStat = processArg(errs, retStat, arg);
			}
		} finally {
			argLock.writeLock().unlock();
		}

		return retStat;
	}

	private boolean processArg(List<String> errs, boolean retStat, String arg) {
		boolean newRet = retStat;

		if (arg.equals("--")) {
			doingArgs = false;
			return newRet;
		}

		// Process an argument
		if (doingArgs && arg.startsWith("-")) {
			String argName = arg;
			String argBody = "";

			// Process arguments to arguments
			int idx = arg.indexOf("=");
			if (idx != -1) {
				argName = arg.substring(0, idx);
				argBody = arg.substring(idx + 1);
			}

			switch (argName) {
			case "-n":
			case "--newline":
				printNL = true;
				break;
			case "-N":
			case "--no-newline":
				printNL = false;
				break;
			case "-v":
			case "--verbose":
				verbosity += 1;
				errStream.louder();
				System.err.printf("[TRACE] Incremented verbosity\n");
				break;
			case "-q":
			case "--quiet":
				verbosity -= 1;
				errStream.quieter();
				System.err.printf("[TRACE] Decremented verbosity\n");
				break;
			case "--verbosity":
				if (argQue.size() < 1) {
					errs.add("[ERROR] No parameter to --verbosity");
					newRet = false;
					break;
				}
				argBody = argQue.pop();
			case "-V":
				try {
					verbosity = Integer.parseInt(argBody);
					errStream.verbosity(verbosity);
					System.err.printf("[TRACE] Set verbosity to %d\n", verbosity);
				} catch (NumberFormatException nfex) {
					String msg = String.format(
							"[ERROR] Invalid verbosity: '%s' is not an integer", argBody);
					errs.add(msg);
					newRet = false;
				}
				break;
			case "--pattern":
				if (argQue.size() < 1) {
					errs.add("[ERROR] No parameter to --pattern");
					newRet = false;
					break;
				}
				argBody = argQue.pop();
			case "-p":
				try {
					pattern = argBody;

					Pattern.compile(argBody);
				} catch (PatternSyntaxException psex) {
					String msg = String.format("[ERROR] Pattern '%s' is invalid: %s",
							pattern, psex.getMessage());
					errs.add(msg);
					newRet = false;
				}
				break;
			case "--file":
				if (argQue.size() < 1) {
					errs.add("[ERROR] No argument to --file");
					newRet = false;
					break;
				}
				argBody = argQue.pop();
			case "-f":
				try (FileInputStream fis = new FileInputStream(argBody);
						Scanner scn = new Scanner(fis)) {
					List<ReplError> ferrs = new ArrayList<>();

					List<ReplPair> lrp = new ArrayList<>();
					lrp = ReplPair.readList(lrp, scn, ferrs, ropts);

					if (ferrs.size() > 0) {
						StringBuilder sb = new StringBuilder();

						String errString = "an error";
						if (ferrs.size() > 1)
							errString = String.format("%d errors", ferrs.size());

						{
							String msg = String.format(
									"[ERROR] Encountered %s parsing data file'%s'\n",
									errString, argBody);
							sb.append(msg);
						}

						for (ReplError err : ferrs) {
							sb.append(String.format("\t%s\n", err));
						}

						errs.add(sb.toString());
						newRet = false;
					}

					replSet.addPairs(lrp);
				} catch (FileNotFoundException fnfex) {
					String msg = String.format(
							"[ERROR] Could not open data file '%s' for input", argBody);
					errs.add(msg);
					newRet = false;
				} catch (IOException ioex) {
					String msg = String.format(
							"[ERROR] Unknown I/O error reading data file '%s': %s",
							argBody, ioex.getMessage());
					errs.add(msg);
					newRet = false;
				}
				break;
			case "--arg-file":
				if (argQue.size() < 1) {
					errs.add("[ERROR] No argument to --arg-file");
					break;
				}
				argBody = argQue.pop();
			case "-F":
				try (FileInputStream fis = new FileInputStream(argBody);
						Scanner scn = new Scanner(fis)) {
					List<String> sl = new ArrayList<>();

					while (scn.hasNextLine()) {
						String ln = scn.nextLine().trim();

						if (ln.equals(""))
							continue;
						if (ln.startsWith("#"))
							continue;

						sl.add(ln);
					}

					processArgs(sl.toArray(new String[0]));
				} catch (FileNotFoundException fnfex) {
					String msg = String.format(
							"[ERROR] Could not open argument file '%s' for input",
							argBody);
					errs.add(msg);
					newRet = false;
				} catch (IOException ioex) {
					String msg = String.format(
							"[ERROR] Unknown I/O error reading input file '%s': %s",
							argBody, ioex.getMessage());
					errs.add(msg);
					newRet = false;
				}
				break;
			case "--input-status":
				if (argQue.size() < 1) {
					errs.add("[ERROR] No argument to --input-status");
					break;
				}
				argBody = argQue.pop();
			case "-I":
				try {
					inputStat = InputStatus.valueOf(argBody.toUpperCase());
				} catch (IllegalArgumentException iaex) {
					String msg = String.format("[ERROR] '%s' is not a valid input status",
							argBody);
					errs.add(msg);
				}
				break;
			default: {
				String msg = String
						.format("[ERROR] Unrecognised CLI argument name '%s'\n", argName);
				errs.add(msg);
				newRet = false;
			}
			}
		} else {
			String tmp = arg;
			// Strip off an escaped initial dash
			if (tmp.startsWith("\\-"))
				tmp = tmp.substring(1);

			processInputFile(tmp);
		}

		return newRet;
	}

	/**
	 * Process a input file.
	 *
	 * @param fle
	 *            Input file to process.
	 * @return Whether we processed succesfully or not.
	 */
	public boolean processInputFile(String fle) {
		List<String> errs = new ArrayList<>();

		boolean stat = processInputFile(errs, fle);
		if (!stat) {
			for (String err : errs) {
				errStream.errorf("%s\n", err);
			}
		}

		return stat;
	}

	/**
	 * Process a input file.
	 *
	 * @param fle
	 *             Input file to process.
	 * @param errs
	 *             List to accumulate errors in.
	 * @return Whether we processed succesfully or not.
	 */
	public boolean processInputFile(List<String> errs, String fle) {
		argLock.readLock().lock();

		// Read in and do replacements on a file
		try {
			if (verbosity > 2) {
				errStream.printf("[TRACE] Reading file (%s) in mode (%s)\n", fle,
						inputStat);
			}

			if (inputStat == InputStatus.ALL) {
				Path pth = Paths.get(fle);

				if (!Files.isReadable(pth)) {
					String msg
							= String.format("[ERROR] File '%s' is not readable\n", fle);
					errs.add(msg);
					return false;
				}

				byte[] inp = Files.readAllBytes(pth);

				String strang = new String(inp, Charset.forName("UTF-8"));

				processString(strang);
			} else if (inputStat == InputStatus.LINE) {
				try (FileInputStream fis = new FileInputStream(fle);
						Scanner scn = new Scanner(fis)) {
					while (scn.hasNextLine()) {
						processString(scn.nextLine());
					}
				}
			} else if (inputStat == InputStatus.REGEX) {
				try (FileInputStream fis = new FileInputStream(fle);
						Scanner scn = new Scanner(fis)) {
					scn.useDelimiter(pattern);

					while (scn.hasNext()) {
						processString(scn.next());
					}
				}
			} else {
				String msg = String.format(
						"[INTERNAL-ERROR] Input status '%s' is not yet implemented\n",
						inputStat);
				errs.add(msg);
				return false;
			}
		} catch (IOException ioex) {
			String msg = String.format(
					"[ERROR] Unknown I/O related error for file '%s'\n\tError was %s",
					fle, ioex.getMessage());
			errs.add(msg);
			return false;
		} finally {
			argLock.readLock().unlock();
		}

		return true;
	}

	/**
	 * Process an input string.
	 *
	 * @param inp
	 *            The input string to process.
	 */
	public void processString(String inp) {
		argLock.readLock().lock();

		try {
			String strang = inp;

			if (verbosity >= 3) {
				errStream.infof(
						"[INFO] Processing replacements for string '%s' in mode %s\n",
						strang, inputStat);
				
				if (!inp.equals(inp.trim())) {
					errStream.infof("[INFO] String '%s' has trailing spaces on it\n", inp);
				}
			}

			strang = replSet.apply(inp);

			outStream.print(strang);
			if (printNL)
				outStream.println();
		} finally {
			argLock.readLock().unlock();
		}
	}

	// Load arguments into the argument queue.
	private void loadQueue(String... args) {
		boolean doArgs = true;
		for (String arg : args) {
			if (arg.equals("--")) {
				doArgs = false;
			}

			// Handle things like -nNv correctly
			if (doArgs) {
				if (arg.startsWith("-") && !arg.startsWith("--")) {
					char[] car = arg.substring(1).toCharArray();

					if (verbosity >= 3) {
						errStream.infof("[INFO] Adding stream of args: %s", car);
					}

					for (char c : car) {
						String argstr = String.format("-%c", c);
						argQue.add(argstr);
					}
				} else {
					argQue.add(arg);
				}
			} else {
				argQue.add(arg);
			}
		}
	}
}