BlackPhreak 5 سال پیش
والد
کامیت
211cc9449c

+ 0 - 0
config.json


BIN
libs/gson-2.8.5.jar


BIN
libs/sqlite-jdbc-3.27.2.1.jar


+ 12 - 0
src/me/blackphreak/CommandHandling/AbstractCommandHandler.java

@@ -0,0 +1,12 @@
+package me.blackphreak.CommandHandling;
+
+public abstract class AbstractCommandHandler {
+	String description;
+	
+	protected AbstractCommandHandler(String desc) {
+		this.description = desc;
+	}
+	
+	public abstract void handle();
+	protected abstract void doneAndReset();
+}

+ 67 - 0
src/me/blackphreak/CommandHandling/CommandHandler.java

@@ -0,0 +1,67 @@
+package me.blackphreak.CommandHandling;
+
+import me.blackphreak.Debug.Debug;
+
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static me.blackphreak.Lib.*;
+
+public class CommandHandler {
+	// use hashmap to remove duplicated handlers
+	private static HashMap</*cmd*/String, AbstractCommandHandler> commandMap = new HashMap<>();
+	
+	static void registerHandler(String cmd, AbstractCommandHandler handler) {
+		commandMap.put(cmd, handler);
+	}
+	
+	/*public static void deregisterHandler(String cmd) {
+		commandMap.remove(cmd);
+	}*/
+	
+	public static void printOptions() {
+		String inp;
+		int chosen;
+		
+		do {
+			System.out.println("Actions:");
+			
+			AtomicInteger i = new AtomicInteger();
+			commandMap.forEach((cmd, handler) -> System.out.println(String.format(
+					"  %2s. %-15s %s",
+					i.incrementAndGet() + "",  // ++i
+					cmd,
+					handler.description
+			)));
+			
+			inp = promptQuestion(String.format(
+					"Please select an action (1 - %d): ",
+					commandMap.size()
+			));
+			
+			// input validation & parsing
+			chosen = tryParseUInt(inp);
+			
+			if (chosen == -1)
+				continue;  // loop
+			
+			if (chosen <= 0
+					|| chosen > commandMap.size())
+			{
+				System.out.println("Invalid action. (Must in between 1 to "+(commandMap.size())+", inclusive)");
+				chosen = -1;  // keep the loop
+			}
+			else
+			{
+				// index offset ("i" is count from "1",
+				//   but array is counting from "0")
+				chosen--;
+			}
+			
+			// loop until a valid option is picked. (chosen != -1)
+		} while (chosen == -1);
+		
+		// pass to the command handler for rest of the actions.
+		((AbstractCommandHandler)(commandMap.values().toArray()[chosen])).handle();
+	}
+}

+ 10 - 0
src/me/blackphreak/CommandHandling/CommandManager.java

@@ -0,0 +1,10 @@
+package me.blackphreak.CommandHandling;
+
+import me.blackphreak.CommandHandling.Handlers.*;
+
+public class CommandManager {
+	public static void registerCommands() {
+		CommandHandler.registerHandler("Search", new SearchCommand("Search student(s) from database with condition(s)"));
+		CommandHandler.registerHandler("Create", new CreateCommand("Insert a new student record to database"));
+	}
+}

+ 139 - 0
src/me/blackphreak/CommandHandling/Handlers/CreateCommand.java

@@ -0,0 +1,139 @@
+package me.blackphreak.CommandHandling.Handlers;
+
+import me.blackphreak.CommandHandling.AbstractCommandHandler;
+import me.blackphreak.Debug.Debug;
+import me.blackphreak.Student.AbstractStudent;
+import me.blackphreak.Student.Types.*;
+
+import static me.blackphreak.Lib.*;
+
+public class CreateCommand extends AbstractCommandHandler {
+	public CreateCommand(String desc) {
+		super(desc);
+	}
+	
+	@Override
+	public void handle() {
+		AbstractStudent stu = null;
+		
+		do {
+			var inp = promptQuestion("Enter type of the student [local | oversea]: ");
+			
+			if (inp.equalsIgnoreCase("local")) {
+				stu = new LocalStudent();
+			}
+			else if (inp.equalsIgnoreCase("oversea")) {
+				stu = new OverseaStudent();
+			}
+			else {
+				System.out.println("Invalid type. Please try again.");
+			}
+		} while (stu == null);
+		
+		System.out.println("  Please provide information about the student:");
+		do {
+			var inp = promptQuestion("    Chinese Name: ");
+			
+			if (stu instanceof LocalStudent)
+			{
+				if (inp.isEmpty() || inp.isBlank())
+					System.out.println("Invalid. Please try again.");
+				else
+					stu.setChtName(inp);
+			}
+			else
+				stu.setChtName("");
+		} while (stu.getChtName() == null);
+		
+		do {
+			var inp = promptQuestion("    English Name: ");
+			
+			if (inp.isEmpty() || inp.isBlank())
+				System.out.println("Invalid. Please try again.");
+			else
+				stu.setEngName(inp);
+		} while (stu.getEngName() == null);
+		
+		do {
+			var inp = promptQuestion("    Home Address: ");
+			
+			if (inp.isEmpty() || inp.isBlank())
+				System.out.println("Invalid. Please try again.");
+			else
+				stu.setHomeAddress(inp);
+		} while (stu.getHomeAddress() == null);
+		
+		do {
+			var inp = promptQuestion("    Mobile Number: ");
+			
+			if (inp.isEmpty()
+					|| inp.isBlank()
+					|| !inp.matches("\\d+"))
+				System.out.println("Invalid. Please try again.");
+			else
+				stu.setMobileNumber(inp);
+		} while (stu.getMobileNumber() == null);
+		
+		do {
+			var inp = promptQuestion("    Nationality: ")
+					.toUpperCase();
+			
+			if (inp.isEmpty()
+					|| inp.isBlank())
+				System.out.println("Invalid nationality. Please try again.");
+			else
+				stu.setNationality(inp);
+		} while (stu.getNationality() == null);
+		
+		do {
+			var inp = promptQuestion("    Current Semester: ");
+			
+			if (inp.isEmpty()
+					|| inp.isBlank()
+					|| !inp.matches("\\d{1,1}"))
+				System.out.println("Invalid. Please try again.");
+			else
+				stu.setSemester(Integer.parseInt(inp));
+		} while (stu.getSemester() == 0);
+		
+		String tryAgain;
+		do {
+			if (!stu.allocateStudentID()) {
+				Debug.err("Failed to allocate studentID!");
+				do {
+					tryAgain = promptQuestion(
+							"** n: will discard all unsaved changes! **\n" +
+									"Try to allocate again? [Y/n]: "
+					);
+					
+					// check for tryAgain option
+					// if not empty (not using the default answer)
+					// AND not a valid option (not either Y nor n)
+					//      prompt the question again
+				} while (!tryAgain.isEmpty() && (
+						!tryAgain.equalsIgnoreCase("Y")
+								|| !tryAgain.equalsIgnoreCase("n")
+				));
+			}
+			else
+				break;
+		} while (tryAgain.isEmpty() || tryAgain.equalsIgnoreCase("Y"));
+		
+		if (stu.getStudentID().isEmpty()) {
+			Debug.err("Failed to allocate studentID. Exiting..");
+			System.exit(1);
+			return;
+		}
+		
+		Debug.info("Student["+stu.getStudentID()+"] created. Saving to database...");
+		
+		// TODO: save to db
+		
+		Debug.info("Success. Exiting entry mode.");
+	}
+	
+	@Override
+	protected void doneAndReset() {
+	
+	}
+}

+ 208 - 0
src/me/blackphreak/CommandHandling/Handlers/SearchCommand.java

@@ -0,0 +1,208 @@
+package me.blackphreak.CommandHandling.Handlers;
+
+import me.blackphreak.CommandHandling.AbstractCommandHandler;
+import me.blackphreak.Database;
+import me.blackphreak.Debug.Debug;
+import me.blackphreak.Student.AbstractStudent;
+import me.blackphreak.Student.Types.LocalStudent;
+import me.blackphreak.Student.Types.OverseaStudent;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static me.blackphreak.Lib.promptQuestion;
+import static me.blackphreak.Lib.tryParseUInt;
+
+public class SearchCommand extends AbstractCommandHandler {
+	private List<String> colNames = new ArrayList<>();
+	private HashMap</*colName*/String, /*searchCondition*/String> selectedCols = new HashMap<>();
+	
+	public SearchCommand(String desc) {
+		super(desc);
+		
+		Database db = Database.getInstance();
+		ResultSet rs = db.query("PRAGMA table_info(Student);");
+		try {
+			while ( rs.next() ) {
+				String col = rs.getString("name");
+				colNames.add(col);
+			}
+		} catch (SQLException e) {
+			db.close();
+		}
+	}
+	
+	@Override
+	public void handle() {
+		boolean nextFlag;
+		do
+		{
+			nextFlag = selectColumnMenu();
+			
+			if (!nextFlag)
+				System.out.println("Please select at least one column to search.");
+		} while (!nextFlag);
+		
+		conditionEntryMenu();
+		
+		// build SQL statement
+		StringBuilder sql = new StringBuilder("SELECT * FROM Student WHERE ");
+		AtomicInteger i = new AtomicInteger();
+		selectedCols.forEach((k, v) -> {
+			if (i.getAndIncrement() > 0) {
+				sql.append(" AND ");
+			}
+			
+			sql.append("`").append(k).append("`");
+			sql.append(" LIKE (\"").append(v).append("\")");
+		});
+		sql.append(";");
+		
+		// do search
+		HashMap</*StudentID*/String, AbstractStudent> students = new HashMap<>();
+		ResultSet rs = Database.getInstance().query(sql.toString());
+		try {
+			do {
+				// read cols
+				String StudentID = rs.getString("StudentID");
+				String HKID = rs.getString("HKID");
+				String VISA = rs.getString("VISA");
+				String Nationality = rs.getString("Nationality");
+				String ChtName = rs.getString("ChtName");
+				String EngName = rs.getString("EngName");
+				String Address = rs.getString("Address");
+				String MobileNum = rs.getString("MobileNum");
+				
+				AbstractStudent stu;
+				if (VISA != null && !VISA.isBlank() && !VISA.isEmpty())
+				{
+					// oversea stu.
+					stu = new OverseaStudent(VISA);
+				}
+				else if (HKID != null && !HKID.isEmpty() && !HKID.isBlank()) {
+					// local stu.
+					stu = new LocalStudent(HKID);
+				}
+				else {
+					Debug.warn("Unknown student type.");
+					continue;
+				}
+				
+				stu.assign(StudentID, ChtName, EngName, Address, MobileNum, Nationality);
+				students.put(StudentID, stu);
+			} while (rs.next());
+		}
+		catch (SQLException e) {
+			// TODO: handling
+			e.printStackTrace();
+		}
+		
+		System.out.println(String.format(
+				"\nResults:\n+ %-13s | %-13s | %-13s | %-20s | %-30s | %-10s | %s",
+				"StudentID",
+				"HKID/VISA",
+				"Nationality",
+				"ChtName",
+				"EngName",
+				"Mobile",
+				"Address"
+		));
+		//                + id            + hkid/visa     + nationality   +
+		System.out.print("+---------------+---------------+---------------+");
+		//                   ChtName              + EngName                        + mobile     + addr
+		System.out.println("----------------------+--------------------------------+------------+-->");
+		
+		students.forEach((k, v) -> System.out.println(
+				String.format("+ %-13s | %-13s | %-13s | %-"+(20-v.getChtName().length())+"s | %-30s | %-10s | %s",
+					v.getStudentID(),
+					(v instanceof LocalStudent) ?
+							((LocalStudent)v).getHKID()
+							: ((OverseaStudent)v).getVisa(),
+					v.getNationality(),
+					v.getChtName(),
+					v.getEngName(),
+					v.getMobileNumber(),
+					v.getHomeAddress()
+				)
+		));
+		
+		//                + id            + hkid/visa     + nationality   +
+		System.out.print("+---------------+---------------+---------------+");
+		//                   ChtName              + EngName                        + mobile     + addr
+		System.out.println("----------------------+--------------------------------+------------+-->");
+		
+		
+		// TODO: allow selection
+	}
+	
+	private boolean selectColumnMenu() {
+		String inp;
+		
+		System.out.println("Search student by (multiple):");
+		
+		AtomicInteger i = new AtomicInteger();
+		// print keys only (display col name)
+		colNames.forEach(colName -> System.out.println(String.format(
+				"  %2s. %s",
+				i.incrementAndGet() + "",
+				colName
+		)));
+		
+		System.out.println("\n** use comma to separate columns **");
+		inp = promptQuestion(String.format(
+				"Select search column(s) (1 - %d): ",
+				colNames.size()
+		));
+		
+		// input validation & parsing
+		List<String> tmp_selectedCols = new ArrayList<>(Arrays.asList(
+				inp.replaceAll("\\s+", "")
+						.split(",")));
+		
+		// remove duplicated & validate column options
+		tmp_selectedCols.stream()
+				.distinct()
+				.filter(targetCol ->
+						!targetCol.isBlank()
+								&& !targetCol.isEmpty()
+								&& targetCol.matches("\\d+")
+				)
+				.sorted()
+				.forEach(targetCol ->
+						selectedCols.put(colNames.get(
+								tryParseUInt(targetCol)-1
+						), ""));
+		
+		return selectedCols.size() > 0;
+	}
+	
+	private void conditionEntryMenu() {
+		
+		selectedCols.keySet().forEach(col -> {
+			System.out.println("\n** use \"_\" as a wildcard character **");
+			System.out.println("** use \"%\" to represent any wildcard character(s) **");
+			System.out.println("** leave it blank to not consider as a condition **");
+			
+			String inp = promptQuestion(String.format(
+					"Search \"%s\" with: ",
+					col
+			));
+			
+			if (!inp.isEmpty() && !inp.isBlank())
+				selectedCols.put(col, inp);
+			else
+				selectedCols.remove(col);
+		});
+	}
+	
+	private void undo() {
+	
+	}
+	
+	@Override
+	protected void doneAndReset() {
+		selectedCols.clear();
+	}
+}

+ 16 - 0
src/me/blackphreak/Config.java

@@ -0,0 +1,16 @@
+package me.blackphreak;
+
+public class Config {
+	public String baseFolder = System.getProperty("user.dir");
+	public String dbFile = baseFolder + "/student.db";
+	
+	private static Config _instance = null;
+	
+	public static Config getInstance() {
+		return (_instance == null ? new Config() : _instance);
+	}
+	
+	public static void setInstance(Config config) {
+		_instance = config;
+	}
+}

+ 70 - 0
src/me/blackphreak/Database.java

@@ -0,0 +1,70 @@
+package me.blackphreak;
+
+import me.blackphreak.Debug.Debug;
+import me.blackphreak.Debug.DebugLevel;
+
+import java.sql.*;
+
+public class Database {
+	// ref.: http://www.runoob.com/sqlite/sqlite-java.html
+	Connection conn;
+	
+	private static Database instance;
+	
+	public static Database getInstance() {
+		return instance != null ? instance : (instance = new Database());
+	}
+	
+	private Database() {
+		connect();
+	}
+	
+	private boolean connect() {
+		try {
+			// connect to sqlite
+			try {
+				Class.forName("org.sqlite.JDBC");
+				conn = DriverManager.getConnection("jdbc:sqlite:students.db");
+			}
+			catch ( Exception e ) {
+				Debug.err("Failed to connect to database. Error: " + e.getMessage());
+				conn.close();
+				System.exit(1);
+			}
+			
+			Debug.log(DebugLevel.INFO.getLevel() | DebugLevel.DEBUG.getLevel(),
+					1,
+					"Connect to database successful."
+			);
+		}
+		catch (Exception ex) {
+			Debug.err("Fatal error! Failed to connect to the database.");
+			ex.printStackTrace();
+			Debug.err("Exiting... [Code: 1]");
+			System.exit(1);
+		}
+		return true;
+	}
+	
+	public ResultSet query(String sql) {
+		ResultSet rs = null;
+		try {
+			Statement stmt = conn.createStatement();
+			rs = stmt.executeQuery(sql);
+		} catch (SQLException e) {
+			Debug.err("Query failed.");
+			e.printStackTrace();
+		}
+		
+		return rs;
+	}
+	
+	public void close() {
+		try {
+			conn.close();
+		}
+		catch (Exception ex) {
+			ex.printStackTrace();
+		}
+	}
+}

+ 108 - 0
src/me/blackphreak/Debug/Debug.java

@@ -0,0 +1,108 @@
+package me.blackphreak.Debug;
+
+import java.util.Arrays;
+
+public class Debug {
+	/**
+	 * Debug Class
+	 *
+	 * log output:
+	 * private log(...) -> "[+][ClassName->MethodName()@LineNumber] Message"
+	 */
+	private static int _currDebugLevel = DebugLevel.ALL.getLevel();
+	
+	private static final String _callerFormat = "%s->%s()@%d";
+	private static final String _logFormat = "[%s][%s] %s";
+	
+	private static String formatCaller(StackTraceElement t) {
+		return String.format(_callerFormat,
+				t.getClassName(),
+				t.getMethodName(),
+				t.getLineNumber()
+		);
+	}
+	
+	// ---- Setter & Getter ----
+	public static int getCurrentDebugLevel() {
+		return _currDebugLevel;
+	}
+	
+	public static void getCurrentDebugLevel(int currDebugLevel) {
+		Debug._currDebugLevel = currDebugLevel;
+	}
+	
+	// ---- Base Function ----
+	
+	/**
+	 * Compare current debug level with the targeted DebugLevel,
+	 *      if greater, make the log.
+	 * @return make the log or not.
+	 */
+	private static boolean isLogLevelMatch(int lv) {
+		return (_currDebugLevel & lv) > 0;
+	}
+	
+	/**
+	 * Log message with caller information
+	 * @param lv log level
+	 * @param offset stack trace offset value
+	 * @param msg log message
+	 */
+	public static void log(int lv, int offset, String msg)
+	{
+		if (!isLogLevelMatch(lv))
+			return;
+		
+		// add 1 offset to get the upper function
+		StackTraceElement t = Thread.currentThread().getStackTrace()[offset + 1];
+		
+		System.out.printf(_logFormat,
+				DebugLevel.getLowest(lv).getSymbol(),
+				formatCaller(t),
+				msg + "\n"
+		);
+	}
+	
+	/**
+	 * Log multi-message with caller information
+	 * @param lv log level
+	 * @param offset stack trace offset value
+	 * @param msgs log messages
+	 */
+	public static void logs(int lv, int offset, String... msgs)
+	{
+		if (!isLogLevelMatch(lv))
+			return;
+		
+		// add 1 offset to get the upper function
+		StackTraceElement t = Thread.currentThread().getStackTrace()[offset + 1];
+		
+		final String header = String.format(_logFormat,
+				DebugLevel.getLowest(lv).getSymbol(),
+				formatCaller(t),
+				""
+		);
+		
+		System.out.print(header + msgs[0] + "\n");
+		
+		// create padding spaces for rest of the messages
+		final String prepend = " ".repeat(header.length() - 2);
+		
+		// skip the first printed message & print rest of the messages
+		Arrays.stream(msgs).skip(1)
+				.forEach(msg -> System.out.println(" |" + prepend + msg));
+	}
+	
+	
+	public static void info(String msg) {
+		log(DebugLevel.INFO.getLevel(), 2, msg);
+	}
+	
+	public static void warn(String msg) {
+		log(DebugLevel.WARN.getLevel(), 2, msg);
+	}
+	
+	public static void err(String msg) {
+		log(DebugLevel.ERR.getLevel(), 2, msg);
+	}
+}

+ 43 - 0
src/me/blackphreak/Debug/DebugLevel.java

@@ -0,0 +1,43 @@
+package me.blackphreak.Debug;
+
+import java.util.Arrays;
+
+public enum DebugLevel {
+	NONE (0, ""),
+	INFO (1, "+"),
+	WARN (2, "!"),
+	ERR  (4, "-"),
+	DEBUG(8, "."),
+	ALL  (Integer.MAX_VALUE, "");
+	
+	private int     _level;
+	private String  _symbol;
+	
+	DebugLevel(int level, String symbol) {
+		this._level     = level;
+		this._symbol    = symbol;
+	}
+	
+	public int getLevel() {
+		return this._level;
+	}
+	
+	public String getSymbol() {
+		return this._symbol;
+	}
+	
+	public static DebugLevel getLowest(int lv) {
+		var tmp = new Object() {
+			DebugLevel lowest = DebugLevel.ALL;
+		};
+		
+		// get the lowest DebugLevel in provided number (variable: lv)
+		Arrays.stream(DebugLevel.values()).forEach(dbLv -> {
+			if ((dbLv.getLevel() & lv) > 0)
+				if (dbLv.getLevel() < tmp.lowest.getLevel())
+					tmp.lowest = dbLv;
+		});
+		
+		return tmp.lowest;
+	}
+}

+ 106 - 0
src/me/blackphreak/FileIO.java

@@ -0,0 +1,106 @@
+package me.blackphreak;
+
+import com.google.gson.Gson;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import me.blackphreak.Debug.Debug;
+import me.blackphreak.Debug.DebugLevel;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+public class FileIO {
+	public static void readConfig(String fullPathToConfigFile) {
+		if (!fullPathToConfigFile.isEmpty()) {
+			File config = new File(fullPathToConfigFile);
+			if (!config.exists()) {
+				// not a valid config file, but lets just continue.
+				Debug.warn("Config file does not exist! Running in default config...");
+			}
+			else {
+				// config file exists, load it.
+				
+				try {
+					var reader = new JsonReader(new FileReader(config));
+					Config.setInstance(new Gson().fromJson(reader, Config.class));
+				}
+				catch (Exception ex) {
+					Debug.err(ex.getMessage());
+					System.exit(1);
+				}
+			}
+		}
+		// else, run in default config
+	}
+	
+	/**
+	 * Save a config file with default config values
+	 */
+	public static void saveConfig() {
+		File config = new File(Config.getInstance().baseFolder + "/config.json");
+		try {
+			JsonWriter writer = new JsonWriter(new FileWriter(config));
+			new Gson().toJson(Config.getInstance(), Config.class, writer);
+		}
+		catch (IOException ex) {
+			Debug.err("Failed to save config file. Error: " + ex.getMessage());
+			return;
+		}
+		
+		Debug.info("Configuration file saved.");
+	}
+	
+	public static void initDir(String fullPathToBaseFolder) throws Exception {
+		if (!fullPathToBaseFolder.isEmpty()) {
+			Config.getInstance().baseFolder = fullPathToBaseFolder;
+		}
+		
+		File tmp_baseFolder = new File(Config.getInstance().baseFolder);
+		if (!tmp_baseFolder.exists() || !tmp_baseFolder.isDirectory()) {
+			if (!tmp_baseFolder.mkdir()) {
+				throw new Exception("Failed to create base directory");
+			}
+			else {
+				Debug.info("Base directory created.");
+			}
+		}
+		
+		File tmp_dbFile = new File(Config.getInstance().dbFile);
+		if (!tmp_dbFile.exists()) {
+			Debug.info("Database does not exist. Copying default one.");
+			
+			if (!copy(
+					Main.class.getResourceAsStream("students.db"),
+					tmp_dbFile.getAbsolutePath()
+				)
+			) {
+				throw new Exception("Failed to copy default database file.");
+			}
+			else {
+				Debug.info("Default database copied. Using the default database...");
+			}
+		}
+		
+		Debug.log(DebugLevel.INFO.getLevel() | DebugLevel.DEBUG.getLevel(),
+				0,
+				"Directory initialization completed."
+		);
+	}
+	
+	
+	// src  : https://stackoverflow.com/a/44077426
+	// usage: copy(getClass().getResourceAsStream("/image/icon.png"), getBasePathForClass(Main.class)+"icon.png");
+	private static boolean copy(InputStream source, String destination) {
+		boolean isSuccess = true;
+		
+		try {
+			Files.copy(source, Paths.get(destination), StandardCopyOption.REPLACE_EXISTING);
+		} catch (IOException ex) {
+			isSuccess = false;
+		}
+		
+		return isSuccess;
+	}
+}

+ 45 - 0
src/me/blackphreak/Lib.java

@@ -0,0 +1,45 @@
+package me.blackphreak;
+
+import me.blackphreak.Debug.Debug;
+import me.blackphreak.Debug.DebugLevel;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+public class Lib {
+	private static BufferedReader reader = new BufferedReader(
+			new InputStreamReader(System.in)
+	);
+	
+	public static String promptQuestion(String question) {
+		try {
+			System.out.print("\n" + question);
+			return reader.readLine();
+		} catch (IOException ex) {
+			Debug.err("Failed to prompt for question ["+question+"]");
+			return "";
+		}
+	}
+	
+	public static int tryParseUInt(String inp) {
+		return tryParseUInt(inp, "The number you provided is invalid. Please try again.");
+	}
+	public static int tryParseUInt(String inp, String failMsg) {
+		int retn = -1;
+		try {
+			
+			if (!inp.isEmpty() && !inp.isBlank())
+			{
+				retn = Integer.parseUnsignedInt(inp);
+			}
+			else
+			{
+				System.out.println(failMsg);
+			}
+		} catch (NumberFormatException ex) {
+			Debug.log(DebugLevel.ERR.getLevel(), 2, failMsg);
+		}
+		return retn;
+	}
+}

+ 93 - 0
src/me/blackphreak/Main.java

@@ -0,0 +1,93 @@
+/**
+ * LICENSE:
+ * The MIT License [https://opensource.org/licenses/MIT]:
+ * Copyright 2019 William Lam @ HKWTC
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * ---------------------------------------------
+ *
+ * Project: Student Information System
+ * Author : William Lam (BlackPhreak)
+ *
+ * ---------------------------------------------
+ *
+ * Used Libraries:
+ * 1. Google Gson
+ *      + https://github.com/google/gson
+ * 2. SQLite
+ *      + https://www.sqlite.org/index.html
+ *      + https://bitbucket.org/xerial/sqlite-jdbc/downloads/
+ */
+
+package me.blackphreak;
+
+import me.blackphreak.CommandHandling.CommandHandler;
+import me.blackphreak.CommandHandling.CommandManager;
+import me.blackphreak.Debug.Debug;
+
+import java.util.HashMap;
+
+public class Main {
+
+    public static void main(String[] args) {
+        HashMap<String, Object> opts = new HashMap<>();
+    
+        for (int i = 0; i < args.length; i++) {
+            // always do "++i" to skip the taken option value.
+        
+            switch (args[i]) {
+                case "--config":
+                    opts.put("config", args[++i]);
+                    break;
+            
+                case "--save_config":
+                    opts.put("save_config", args[++i]);
+                    break;
+            
+                case "-h":
+                case "--help":
+                    printHelp();
+                    return;
+            }
+        }
+    
+        /*if (args.length == 0) {
+            printHelp();
+            return;
+        }*/
+    
+        // read & execute options with order:
+        var config = (String) opts.getOrDefault("config", "");
+        FileIO.readConfig(config);
+        
+        // connect to database
+        Database db = Database.getInstance();
+        
+        // save config
+        var save_config = opts.get("save_config");
+        if (save_config != null) {
+            FileIO.saveConfig();
+        }
+        
+        // register commands
+        CommandManager.registerCommands();
+        
+        // command mode (keep looping until exit)
+        CommandHandler.printOptions();
+        
+        // close the db connection
+        db.close();
+    }
+    
+    private static void printHelp() {
+        System.out.println("----------- Help -----------");
+        System.out.println("--config <file>          : Indicate program to use the specific configuration file\n");
+        System.out.println("--save_config <file>     : Save the running configuration file.\n");
+        System.out.println("-h  --help               : Print this help message\n\n");
+    }
+}

+ 138 - 0
src/me/blackphreak/Student/AbstractStudent.java

@@ -0,0 +1,138 @@
+package me.blackphreak.Student;
+
+import me.blackphreak.Debug.Debug;
+
+import java.util.HashMap;
+
+public abstract class AbstractStudent {
+	
+	private String _studentID;
+	private String _chtName;  // chinese-traditional
+	private String _engName;  // english
+	private String _homeAddress;
+	private String _mobileNumber;
+	private HashMap</*subject*/String, /*grade*/String> _grades = new HashMap<>();
+	private int    _semester;  // current semester
+	private String _nationality;
+	
+	public void assign(String stuID,
+	                       String chtName,
+	                       String engName,
+	                       String homeAddress,
+	                       String mobileNumber,
+	                       String nationality)
+	{
+		this._studentID = stuID;
+		this._chtName = chtName;
+		this._engName = engName;
+		this._homeAddress = homeAddress;
+		this._mobileNumber = mobileNumber;
+		this._nationality = nationality;
+	}
+	
+	public void printInfo() {
+		System.out.println(String.format("Student ID [%s]", getStudentID()));
+		System.out.println(String.format("    %13s: %s", "Chinese Name", getChtName()));
+		System.out.println(String.format("    %13s: %s", "English Name", getEngName()));
+		System.out.println(String.format("    %13s: %s", "Home Address", getHomeAddress()));
+		System.out.println(String.format("    %13s: %s", "Mobile Number", getMobileNumber()));
+		System.out.println(String.format("    %13s: %s", "Nationality", getNationality()));
+		System.out.println(String.format("    %13s: %d", "Semester", getSemester()));
+		
+		printInfoExtra();
+	}
+	
+	/**
+	 * allocate a student ID from database and assign to the student directly
+	 * @return assign success / fail
+	 */
+	public boolean allocateStudentID() {
+		// TODO: get last studentID and then +1
+		String id = "";
+		return updateStudentID(id);
+	}
+	
+	/**
+	 * update the studentID and check for duplicate
+	 * @param newStudentID a new studentID that want to assign with
+	 * @return false when duplicated
+	 */
+	public boolean updateStudentID(String newStudentID) {
+		// TODO: check for duplicate
+		if (true) {
+			// if duplicated
+			Debug.err("Failed to update studentID ["+getStudentID()+"] to ["+newStudentID+"]");
+			return false;
+		}
+		
+		// no duplicate
+		if (true) {
+			this._studentID = newStudentID;
+		}
+		return true;
+	}
+	
+	public abstract void printInfoExtra();
+	
+	// --- getter/setter ---
+	public String getStudentID() {
+		return _studentID;
+	}
+	
+	public String getChtName() {
+		return _chtName;
+	}
+	
+	public void setChtName(String _chtName) {
+		this._chtName = _chtName;
+	}
+	
+	public String getEngName() {
+		return _engName;
+	}
+	
+	public void setEngName(String _engName) {
+		this._engName = _engName;
+	}
+	
+	public String getHomeAddress() {
+		return _homeAddress;
+	}
+	
+	public void setHomeAddress(String _homeAddress) {
+		this._homeAddress = _homeAddress;
+	}
+	
+	public String getMobileNumber() {
+		return _mobileNumber;
+	}
+	
+	public void setMobileNumber(String _mobileNumber) {
+		this._mobileNumber = _mobileNumber;
+	}
+	
+	public HashMap<String, String> getGrades() {
+		return _grades;
+	}
+	
+	public void setGrades(HashMap<String, String> _grades) {
+		this._grades = _grades;
+	}
+	
+	public int getSemester() {
+		return _semester;
+	}
+	
+	public void setSemester(int _semester) {
+		this._semester = _semester;
+	}
+	
+	public String getNationality() {
+		return _nationality;
+	}
+	
+	public void setNationality(String nationality) {
+		this._nationality = nationality;
+	}
+	
+}

+ 41 - 0
src/me/blackphreak/Student/Types/LocalStudent.java

@@ -0,0 +1,41 @@
+package me.blackphreak.Student.Types;
+
+import me.blackphreak.Student.AbstractStudent;
+
+import static me.blackphreak.Lib.*;
+
+public class LocalStudent extends AbstractStudent {
+	private String _HKID;
+	
+	public LocalStudent(String HKID) {
+		this._HKID = HKID;
+	}
+	
+	public LocalStudent() {
+		do {
+			var inp = promptQuestion("    HKID: ")
+					.toUpperCase();
+			
+			// HKID regex pattern src: https://www.regextester.com/97968
+			if (inp.isEmpty()
+					|| inp.isBlank()
+					|| !inp.matches("^[A-Z]{1,2}[0-9]{6}[0-9A-F]{1}"))
+				System.out.println("Invalid HKID. Please try again.");
+			else
+				this.setHKID(inp);
+		} while (this.getHKID() == null);
+	}
+	
+	public String getHKID() {
+		return _HKID;
+	}
+	
+	public void setHKID(String HKID) {
+		this._HKID = HKID;
+	}
+	
+	@Override
+	public void printInfoExtra() {
+		System.out.println(String.format("    %13s: %s", "HKID", getHKID()));
+	}
+}

+ 39 - 0
src/me/blackphreak/Student/Types/OverseaStudent.java

@@ -0,0 +1,39 @@
+package me.blackphreak.Student.Types;
+
+import me.blackphreak.Student.AbstractStudent;
+
+import static me.blackphreak.Lib.promptQuestion;
+
+public class OverseaStudent extends AbstractStudent {
+	private String _visa;  // null-able
+	
+	public OverseaStudent(String visa) {
+		this._visa = visa;
+	}
+	
+	public OverseaStudent() {
+		do {
+			var inp = promptQuestion("    VISA: ")
+					.toUpperCase();
+			
+			if (inp.isEmpty()
+					|| inp.isBlank())
+				System.out.println("Invalid VISA. Please try again.");
+			else
+				this.setVisa(inp);
+		} while (this.getVisa() == null);
+	}
+	
+	public String getVisa() {
+		return _visa;
+	}
+	
+	public void setVisa(String visa) {
+		this._visa = visa;
+	}
+	
+	@Override
+	public void printInfoExtra() {
+		System.out.println(String.format("    %13s: %s", "Visa", getVisa()));
+	}
+}

BIN
students.db