com.mijecu25.sqlplus.SQLPlus.java Source code

Java tutorial

Introduction

Here is the source code for com.mijecu25.sqlplus.SQLPlus.java

Source

package com.mijecu25.sqlplus;

import java.io.BufferedReader;
import java.io.Console;
import java.io.FileReader;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Properties;

import com.mijecu25.sqlplus.compiler.core.statement.StatementDefault;
import jline.console.UserInterruptException;
import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.mijecu25.messages.Messages;
import com.mijecu25.sqlplus.compiler.core.statement.Statement;
import com.mijecu25.sqlplus.connection.SQLPlusConnection;
import com.mijecu25.sqlplus.connection.SQLPlusMySQLConnection;
import com.mijecu25.sqlplus.parser.SQLPlusLex;
import com.mijecu25.sqlplus.parser.SQLPlusParser;

import jline.TerminalFactory;
import jline.console.ConsoleReader;

/**
 * SQLPlus add alerts to your sql queries.
 * 
 * @author Miguel Velez - miguelvelezmj25
 * @version 0.1.0.27
 */
public class SQLPlus {

    public static final String PROGRAM_NAME = "SQLPlus";
    private static final String EXIT = "exit";
    private static final String QUIT = "quit";
    private static final String PROMPT = "sqlplus> ";
    private static final char END_OF_COMMAND = ';';
    private static final String WAIT_FOR_END_OF_COMMAND = "      -> ";
    private static final String LICENSE_FILE = "LICENSE";
    private static final String APPLICATION_PROPERTIES_FILE = "application.properties";
    private static final String APPLICATION_PROPERTIES_FILE_VERSION = "application.version";

    private static SQLPlusConnection sqlPlusConnection;
    private static ConsoleReader console;

    private static final Logger logger = LogManager.getLogger(SQLPlus.class);

    public static void main(String[] args) throws IOException {
        // Create and load the properties from the application properties file
        Properties properties = new Properties();
        properties.load(SQLPlus.class.getClassLoader().getResourceAsStream(SQLPlus.APPLICATION_PROPERTIES_FILE));

        SQLPlus.logger.info("Initializing " + SQLPlus.PROGRAM_NAME + " version "
                + properties.getProperty(SQLPlus.APPLICATION_PROPERTIES_FILE_VERSION));

        // Check if the user is using a valid console (i.e. not from Eclipse)
        if (System.console() == null) {
            // The Console object for the JVM could not be found. Alert the user 
            SQLPlus.logger.fatal(Messages.FATAL + "A JVM Console object was not found. Try running "
                    + SQLPlus.PROGRAM_NAME + "from the command line");
            System.out.println(
                    Messages.FATAL + SQLPlus.PROGRAM_NAME + " was not able to find your JVM's Console object. "
                            + "Try running " + SQLPlus.PROGRAM_NAME + " from the command line.");

            SQLPlus.exitSQLPlus();

            SQLPlus.logger.fatal(Messages.FATAL + Messages.QUIT_PROGRAM_ERROR(PROGRAM_NAME));
            return;
        }

        // UI intro
        System.out.println("Welcome to " + SQLPlus.PROGRAM_NAME
                + "! This program has a DSL to add alerts to various SQL DML events.");
        System.out.println("Be sure to use " + SQLPlus.PROGRAM_NAME + " from the command line.");
        System.out.println();

        // Get the version
        System.out.println("Version: " + properties.getProperty(SQLPlus.APPLICATION_PROPERTIES_FILE_VERSION));
        System.out.println();

        // Read the license file
        BufferedReader bufferedReader;
        bufferedReader = new BufferedReader(new FileReader(SQLPlus.LICENSE_FILE));

        // Read a line
        String line = bufferedReader.readLine();

        // While the line is not null
        while (line != null) {
            System.out.println(line);

            // Read a new lines
            line = bufferedReader.readLine();
        }

        // Close the buffer
        bufferedReader.close();
        System.out.println();

        // Create the jline console that allows us to remember commands, use arrow keys, and catch interruptions
        // from the user
        SQLPlus.console = new ConsoleReader();
        SQLPlus.console.setHandleUserInterrupt(true);

        try {
            // Get credentials from the user
            SQLPlus.logger.info("Create SQLPlusConnection");
            SQLPlus.createSQLPlusConnection();
        } catch (NullPointerException | SQLException | IllegalArgumentException e) {
            // NPE: This exception can occur if the user is running the program where the JVM Console
            // object cannot be found.
            // SQLE: TODO should I add here the error code?
            // This exception can occur when trying to establish a connection
            // IAE: This exception can occur when trying to establish a connection
            SQLPlus.logger
                    .fatal(Messages.FATAL + Messages.FATAL_EXIT(SQLPlus.PROGRAM_NAME, e.getClass().getName()));
            System.out.println(Messages.FATAL + Messages.FATAL_EXCEPTION_ACTION(e.getClass().getSimpleName()) + " "
                    + Messages.CHECK_LOG_FILES);
            SQLPlus.exitSQLPlus();

            SQLPlus.logger.fatal(Messages.FATAL + Messages.QUIT_PROGRAM_ERROR(SQLPlus.PROGRAM_NAME));
            return;
        } catch (UserInterruptException uie) {
            SQLPlus.logger.warn(Messages.WARNING + "The user typed an interrupt instruction.");
            SQLPlus.exitSQLPlus();

            return;
        }

        System.out.println("Connection established! Commands end with " + SQLPlus.END_OF_COMMAND);
        System.out.println("Type " + SQLPlus.EXIT + " or " + SQLPlus.QUIT + " to exit the application ");

        try {
            // Execute the input scanner
            while (true) {
                // Get a line from the user until the hit enter (carriage return, line feed/ new line).
                System.out.print(SQLPlus.PROMPT);
                try {
                    line = SQLPlus.console.readLine().trim();
                } catch (NullPointerException npe) {
                    // TODO test this behavior
                    // If this exception is catch, it is very likely that the user entered the end of line command.
                    // This means that the program should quit.
                    SQLPlus.logger.warn(Messages.WARNING + "The input from the user is null. It is very likely that"
                            + "the user entered the end of line command and they want to quit.");
                    SQLPlus.exitSQLPlus();

                    return;
                }

                // If the user did not enter anything
                if (line.isEmpty()) {
                    // Continue to the next iteration
                    continue;
                }

                if (line.equals(".")) {
                    line = "use courses;";
                }

                if (line.equals("-")) {
                    line = "select created_at from classes;";
                }

                if (line.equals("--")) {
                    line = "select name, year from classes;";
                }

                if (line.equals("*")) {
                    line = "select * from classes;";
                }

                if (line.equals("x")) {
                    line = "select name from classes, classes;";
                }

                if (line.equals("e")) {
                    line = "select * feom classes;";
                }

                // Logic to quit
                if (line.equals(SQLPlus.QUIT) || line.equals(SQLPlus.EXIT)) {
                    SQLPlus.logger.info("The user wants to quit " + SQLPlus.PROGRAM_NAME);
                    SQLPlus.exitSQLPlus();
                    break;
                }

                // Use a StringBuilder since jline works weird when it has read a line. The issue we were having was with the
                // end of command logic. jline does not keep the input from the user in the variable that was stored in. Each
                // time jline reads a new line, the variable is empty
                StringBuilder query = new StringBuilder();
                query.append(line);

                // While the user does not finish the command with the SQLPlus.END_OF_COMMAND
                while (query.charAt(query.length() - 1) != SQLPlus.END_OF_COMMAND) {
                    // Print the wait for command prompt and get the next line for the user
                    System.out.print(SQLPlus.WAIT_FOR_END_OF_COMMAND);
                    query.append(" ");
                    line = StringUtils.stripEnd(SQLPlus.console.readLine(), null);
                    query.append(line);
                }

                SQLPlus.logger.info("Raw input from the user: " + query);

                try {
                    Statement statement;

                    try {
                        // Execute the antlr code to parse the user input
                        SQLPlus.logger.info("Will parse the user input to determine what to execute");
                        ANTLRStringStream input = new ANTLRStringStream(query.toString());
                        SQLPlusLex lexer = new SQLPlusLex(input);
                        CommonTokenStream tokens = new CommonTokenStream(lexer);
                        SQLPlusParser parser = new SQLPlusParser(tokens);

                        statement = parser.sqlplus();
                    } catch (RecognitionException re) {
                        // TODO move this to somehwere else
                        //                        String message = Messages.WARNING + "You have an error in your SQL syntax. Check the manual"
                        //                                + " that corresponds to your " + SQLPlus.sqlPlusConnection.getCurrentDatabase()
                        //                                + " server or " + SQLPlus.PROGRAM_NAME + " for the correct syntax";
                        //                        SQLPlus.logger.warn(message);
                        //                        System.out.println(message);
                        statement = new StatementDefault();
                    }

                    statement.setStatement(query.toString());
                    SQLPlus.sqlPlusConnection.execute(statement);
                } catch (UnsupportedOperationException uoe) {
                    // This exception can occur when the user entered a command allowed by the parsers, but not currently
                    // supported by SQLPlus. This can occur because the parser is written in such a way that supports
                    // the addition of features.
                    SQLPlus.logger.warn(Messages.WARNING + uoe);
                    System.out.println(
                            Messages.WARNING + Messages.FATAL_EXCEPTION_ACTION(uoe.getClass().getSimpleName()) + " "
                                    + Messages.CHECK_LOG_FILES);
                    SQLPlus.logger.warn(Messages.WARNING + "The previous command is not currently supported.");
                }

            }
        } catch (IllegalArgumentException iae) {
            // This exception can occur when a command is executed but it had illegal arguments. Most likely
            // it is a programmer's error and should be addressed by the developer.
            SQLPlus.logger
                    .fatal(Messages.FATAL + Messages.FATAL_EXIT(SQLPlus.PROGRAM_NAME, iae.getClass().getName()));
            SQLPlus.exitSQLPlus();

            SQLPlus.logger.fatal(Messages.FATAL + Messages.QUIT_PROGRAM_ERROR(SQLPlus.PROGRAM_NAME));
        } catch (UserInterruptException uie) {
            SQLPlus.logger.warn(Messages.WARNING + "The user typed an interrupt instruction.");
            SQLPlus.exitSQLPlus();
        }
    }

    /**
     * Create an SQLPlusConnection by taking the credentials from the user.
     *
     * @throws IOException if there is an I/O error while reading input from the user.
     * @throws SQLException if there is an error while establishing a connection.
     */
    private static void createSQLPlusConnection() throws IOException, SQLException {
        if (false) {
            System.out.println("You will now enter the credentials to connect to your database");

            // Add credentials
            System.out.print(SQLPlus.PROMPT + "Host(default " + SQLPlusConnection.getDefaultHost() + "): ");
            String host = SQLPlus.console.readLine().trim();
            SQLPlus.logger.info("User entered host:" + host);
            // TODO validate host        

            //        if(!host.isEmpty()) {
            //            // The Console object for the JVM could not be found. Alert the user and throw a
            //            // NullPointerException that the caller will handle
            //            SQLPlus.logger.fatal(Messages.FATAL + "The user wants to use a host that is not supported");
            //            System.out.println(Messages.ERROR + SQLPlus.PROGRAM_NAME + " does not support the host that you entered");
            //            
            //            SQLPlus.logger.info("Throwing a " + IllegalArgumentException.class.getSimpleName() + " to the "
            //                    + "calling class");
            //            throw new IllegalArgumentException();  
            //        }

            System.out.print(SQLPlus.PROMPT + "Database(default " + SQLPlusConnection.getDefaultDatabase() + "): ");
            String database = SQLPlus.console.readLine().trim();
            SQLPlus.logger.info("User entered database:" + database);

            if (database.isEmpty()) {
                database = SQLPlusConnection.getDefaultDatabase();
                SQLPlus.logger.info("Using default database:" + database);
            }

            String port = "";

            // While the port is not numeric
            while (!StringUtils.isNumeric(port)) {
                System.out.print(SQLPlus.PROMPT + "Port (default " + SQLPlusConnection.getDefaultPort() + "): ");
                port = SQLPlus.console.readLine().trim();
                SQLPlus.logger.info("Port entered: " + port);
                SQLPlus.logger.info("Port string length: " + port.length());

                // If the port is empty
                if (port.isEmpty()) {
                    // Assume that the user wants to use the default port. Continue to the next step
                    break;
                }

                // If the port has more than 5 numbers or is not numberic 
                if (port.length() > 5 || !StringUtils.isNumeric(port)) {
                    SQLPlus.logger.warn("The user provided an invalid port number: " + port);
                    System.out.println(
                            Messages.WARNING + "You need to provided a valid port number " + "from 0 to 65535");

                    // Set the port to the empty string to ask the user again
                    port = "";
                }
            }
            SQLPlus.logger.info("User entered port:" + port);

            String username = "";

            // While the username is empty
            while (username.isEmpty()) {
                System.out.print(SQLPlus.PROMPT + "Username: ");
                username = SQLPlus.console.readLine().trim();

                // If the username is empty
                if (username.isEmpty()) {
                    SQLPlus.logger.warn("The user did not provide a username");
                    System.out.println(Messages.WARNING + "You cannot have an empty username");
                }
            }
            SQLPlus.logger.info("User entered username:" + username);

            // Reset the jline console since we are going to use the regular console to securely get the password
            SQLPlus.resetConsole();

            // Get the console for safe password entry
            Console javaConsole = System.console();

            // If the console is null
            if (javaConsole == null) {
                // The Console object for the JVM could not be found. Alert the user and throw a
                // NullPointerException that the caller will handle
                SQLPlus.logger.fatal("A JVM Console object to enter a password was not found");
                System.out.println(
                        Messages.ERROR + SQLPlus.PROGRAM_NAME + " was not able to find your JVM's Console object. "
                                + "Try running " + SQLPlus.PROGRAM_NAME + " from the command line.");

                SQLPlus.logger.info(
                        "Throwing a " + NullPointerException.class.getSimpleName() + " to the " + "calling class");
                throw new NullPointerException();
            }

            // Read the password without echoing the result
            char[] password = javaConsole.readPassword("%s", SQLPlus.PROMPT + "Password:");

            // If the password is null
            if (password == null) {
                // The Console object for the JVM could not be found. Alert the user and throw a
                // NullPointerException that the caller will handle
                SQLPlus.logger.fatal("The password captured by the JVM Console object returned null");
                System.out.println(
                        Messages.ERROR + SQLPlus.PROGRAM_NAME + " was not able to get the password you entered from"
                                + "your JVM's Console object. Try running " + SQLPlus.PROGRAM_NAME
                                + " from the command line or a different" + "terminal program");

                SQLPlus.logger.info(
                        "Throwing a " + NullPointerException.class.getSimpleName() + " to the " + "calling class");
                throw new NullPointerException();
            }
            SQLPlus.logger.info("User entered some password");
            System.out.println();

            // Create a connection based on the database system
            switch (database) {
            case SQLPlusMySQLConnection.MYSQL:
                // If the default port and host are used
                if (port.isEmpty() && host.isEmpty()) {
                    SQLPlus.logger.info("Connection with username, password");
                    sqlPlusConnection = SQLPlusMySQLConnection.getConnection(username, password);
                }
                // If the default port is used
                else if (port.isEmpty()) {
                    SQLPlus.logger.info("Connection with username, password, and host");
                    sqlPlusConnection = SQLPlusMySQLConnection.getConnection(username, password, host);
                }
                // All the values were provided by the user
                else {
                    SQLPlus.logger.info("Connection with all credentials");
                    sqlPlusConnection = SQLPlusMySQLConnection.getConnection(username, password, host, port);
                }
                break;
            default:
                // Database entered is not supported
                SQLPlus.logger.fatal(Messages.FATAL + "The database system " + database + " is not supported");
                System.out.println(
                        Messages.ERROR + SQLPlus.PROGRAM_NAME + " does not support the database that you entered");

                SQLPlus.logger.info("Throwing a " + IllegalArgumentException.class.getSimpleName() + " to the "
                        + "calling class");
                throw new IllegalArgumentException();
            }

            // Delete any traces of password in memory by filling the password array with with random characters
            // to minimize the lifetime of sensitive data in memory. Then call the garbage collections
            java.util.Arrays.fill(password, Character.MIN_VALUE);
            System.gc();

            // Recreate the jline console
            SQLPlus.console = new ConsoleReader();
            SQLPlus.console.setHandleUserInterrupt(true);
        }

        // TODO remove this which is for testing
        SQLPlus.logger.info("Connection with username, password, and host");
        SQLPlusConnection sqlPlusConnection = SQLPlusMySQLConnection.getConnection("sqlplus", new char[0],
                SQLPlusConnection.getDefaultHost());

        // TODO this does have to be in the final code
        SQLPlus.logger.info("Created and returning a SQLPlusConnection " + sqlPlusConnection);
        SQLPlus.sqlPlusConnection = sqlPlusConnection;
    }

    /**
     * Exit SQLPlus.
     */
    private static void exitSQLPlus() {
        // If there is a SQLPlusConnection
        if (SQLPlus.sqlPlusConnection != null) {
            // Disconnect from the database
            SQLPlus.logger.info("Attempting to disconnect the SQLPlusConnection");
            SQLPlus.sqlPlusConnection.disconnect();
            SQLPlus.logger.info("Disconnected the SQLPlusConnection");
        }

        // Reset the console from jline
        SQLPlus.logger.info("Reset the console from jline");
        SQLPlus.resetConsole();

        SQLPlus.logger.info("Quitting " + SQLPlus.PROGRAM_NAME);
        System.out.println("Bye");
    }

    /**
     * Reset the console from the changes that jline has done.
     */
    private static void resetConsole() {
        // Reset the console
        try {
            SQLPlus.logger.info("About to reset the console from jline");
            TerminalFactory.get().restore();
            SQLPlus.logger.info("Reset the console from jline");
        } catch (Exception e) {
            // This exception might never occur, but it is good practice to handle it
            SQLPlus.logger.warn(
                    Messages.WARNING + "Error when attempting to reset the console from the changes made by jline",
                    e);
            System.out.println(
                    Messages.WARNING + "There was a error when trying to reset your console to its normal state");
            System.out.println(e.getMessage());
            System.out
                    .println(Messages.WARNING + "Close this console window and open a new one to avoid any issues");
        }
    }

}