/**
 * PostgreSQL Java版コマンドラインインタフェース
 * (PostgreSQLのサンプル /src/interface/jdbc/exsample/psql.java を改造して作成)
 * 
 */

/*
 * 更新履歴
 * 
 * 2006/05/16  標準入力からの文字列の読み込みをStreamTokenierからBufferedReaderに変更
 *             readLine()を使用.(文字落ち対策)
 * version 0.41 Ver0.4におけるSQL入力ファイルに関する実装漏れを修正
 * version 0.4 接続先情報選択方式をコマンドライン引数方式に変更
 *             COMMONS CLIを利用
 *             ソースコードのエンコードをUTF-8に変更
 * version 0.3 接続先情報選択方式をファイル入力方式に変更
 * version 0.2 接続先情報選択方式を追加(ソース内で管理)
 * version 0.1 SQLが記述されたファイル入力機能を追加
 * 
 */

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

/*
 * This example application demonstrates some of the drivers other features
 * by implementing a simple psql replacement in Java.
 *
 */

/**
 * 接続先DB情報を取得し、コマンドラインで与えられたSQL文を実行・結果を返す
 */
public class Jpsql
{
	/* SQL文パース用メンバ */
	private StringBuffer m_sql = new StringBuffer();
	private boolean m_isQuoteChar = false;
	//行コメントの判別フラグ
	private boolean m_lineComment = false;
	//Cスタイルコメントのコメント判別：ネスト階層カウンタ
	private int m_slashStarComments = 0;

	/** The connection to the database */
	private static Connection db;
	
	/** データベース接続情報
	 * main()内でのコマンドライン引数解析の結果取得した結果が入る
	 */
	private static String driver = null;
	private static String url = null;
	private static String usr = null;
	private static String pwd = null;
	private static String inputfile = null;

	
	/** Our statement to run queries with */
	Statement	st;
	/** This defines the structure of the database */
	DatabaseMetaData dbmd;
	/** Added by CWJ to permit \q command */
	boolean done = false;

	String newLineCode = System.getProperty("line.separator");
	
	public Jpsql(String args[]) throws SQLException
	{
		
		/*
		String url = args[0];
		String usr = args[1];
		String pwd = args[2];
		*/
		
		// main()内でのオプション解析に任せるため削除
		//　入力値のチェックもmain()で行う
		///* 接続情報を設定 */		
		//
		//ConnInfo connInfo = new ConnInfo(args[0]);
		//String driver = connInfo.getDriver();
		//String url = connInfo.getUrl();
		//String usr = connInfo.getUser();
		//String pwd = connInfo.getPass();

		// Load the driver
		//Class.forName("org.postgresql.Driver");
		
//		System.out.println("driver = " + driver);
//		System.out.println("url = " + url);
//		System.out.println("user = " + usr);

		try
		{
			Class.forName(driver);
		}
		catch (ClassNotFoundException e)
		{
			e.printStackTrace();
			System.exit(-1);
		}

		// Connect to database
		System.out.println("Connecting to Database URL = " + url + " (user:" + usr +")");
		db = DriverManager.getConnection(url, usr, pwd);

		dbmd = db.getMetaData();
		st = db.createStatement();

		// This prints the backend's version
		System.out.println("Connected to " + dbmd.getDatabaseProductName() + " " + dbmd.getDatabaseProductVersion());

		System.out.println();

		/* SQLファイル入力がある場合、ファイル名を設定後一括処理する */
//		if(args.length == 2){
//			inputfile = args[1];
		if(inputfile != null) {
			System.out.println("Using SQL file input from " + inputfile);
			ProcessSqlFile(inputfile);
			System.exit(0);//以降の処理は行わない
		}
		else{
			/* 入力プロンプトを表示し、入力されたSQLを逐次処理する */
			// This provides us the means of reading from stdin
//			StreamTokenizer input = new StreamTokenizer(new InputStreamReader(System.in));
//			input.resetSyntax();
//			input.slashSlashComments(true); // allow // as a comment delimiter
//			input.eolIsSignificant(false);	// treat eol's as spaces
//			input.wordChars(32, 126);
			// input.whitespaceChars(59, 59);	// セミコロンをスペースには変換しない
												// バックスラッシュコマンド実行時にはセミコロンを入力する必要は無い
			// input.quoteChar(39); *** CWJ: messes up literals in query string ***

			BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
			
			// Now the main loop.
			int lineno = 1;
			/* プロンプト表示フラグに従ってプロンプト表示 */
			
			try {
//				while (tt != StreamTokenizer.TT_EOF && ! done)
				while (!done)
				{	
					System.out.print("[" + lineno + "] ");
					System.out.flush();

					// System.out.println(input.ttype);
	
					StringBuffer line = new StringBuffer();

					int contents;

					while ( (contents = in.read() ) != -1)
					{
						line.append((char)contents);

						if ( /*contents == '\r' ||*/
							contents == '\n' )
						{
							processLine(line.toString());

							lineno++;
							break;
						}
					}

					/* EOF */
					if ( contents == -1 )
						break;
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		System.out.println("Now closing the connection");
		st.close();
		db.close();
	}

	/**
	 * 入力されたステートメントを実行する
	 * This processes a statement
	 * 
	 * @param line コマンドラインより入力された文字列
	 * @throws SQLException
	 */
	public void processLine(String line) throws IOException
	{
		StringBuffer backSlashCmd = null;
		boolean isBackSlashCmd = false;

		StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(line));
		tokenizer.resetSyntax();
		tokenizer.wordChars('a', 'z');
		tokenizer.wordChars('A', 'Z');
		tokenizer.wordChars('_', '_');
		tokenizer.wordChars('.', '.');
		tokenizer.wordChars('0', '9');		
		
		tokenizer.ordinaryChar('/');
		
		tokenizer.eolIsSignificant(true);

		int token = StreamTokenizer.TT_EOF;
		while ((token = tokenizer.nextToken()) != StreamTokenizer.TT_EOF) {

			
			//バックスラッシュコマンドが指定されていたら
			if(isBackSlashCmd){
				if(token == StreamTokenizer.TT_WORD){
					backSlashCmd.append(tokenizer.sval); 
					
				}else if(token == StreamTokenizer.TT_EOL){
					//改行が検出されたら
					m_sql.append((char)tokenizer.ttype);
					//バックスラッシュコマンドの実行
					try {
						processSlashCommand(backSlashCmd.toString().trim());
					} catch (SQLException e) {
						System.out.println(e.getMessage());
					}
				}else{
					backSlashCmd.append((char)tokenizer.ttype); 
				}
				continue;
			}

			//Cスタイルコメントが指定されていたら
			if(m_slashStarComments > 0){

				if(  token == '*' ){
					
					//次の文字が、/ならばコメントネスト減算
					token = tokenizer.nextToken();
					if(token == '/'){
						m_slashStarComments--;
					}else{
						tokenizer.pushBack();
					}
					
				}else if(token == '/'){

					//次の文字が、*ならばコメントネスト加算
					token = tokenizer.nextToken();
					if(token == '*'){
						m_slashStarComments++;
					}else{
						tokenizer.pushBack();
					}
					
				}
				continue;
				
			}
			
			//--行コメントが指定されていたら
			if(m_lineComment){
				//改行コードが来るまで読み飛ばし
				if(  token == StreamTokenizer.TT_EOL ){
					m_lineComment = false;
				}else{
					continue;
				}
			}
			
			switch (token) {
			
				case ';' :
					m_sql.append((char)tokenizer.ttype); 
					if(!m_isQuoteChar){
						System.out.println("SQL:" + m_sql.toString());
						String sql = m_sql.toString();
						m_sql = new StringBuffer();	//実行SQLバッファ初期化

						try {
							//SQL実行	
							boolean type = st.execute(sql);
							boolean loop = true;
							//boolean type = true;
							while (loop)
							{
								if (type)
								{
									// A ResultSet was returned
									ResultSet rs = st.getResultSet();
									//ResultSet rs = st.executeQuery(ExecLine);
									displayResult(rs);
									//loop = false;
								}
								else
								{
									int count = st.getUpdateCount();
									
									if (count == -1)
									{
										// This indicates nothing left
										loop = false;
									}
									else
									{
										// An update count was returned
										System.out.println("Updated " + st.getUpdateCount() + " rows");
									}
								}

								if (loop)
									type = st.getMoreResults();
							}
						}
						catch (SQLException e)
						{
							System.out.println( e.getMessage() );
						}
					}
					break;
			
				case StreamTokenizer.TT_WORD :
					m_sql.append(tokenizer.sval);
					break;
		
				case '\'':
					// クオートされた文字列の中にシングルクオートが出た場合
					if(m_isQuoteChar ){
						// シングルクオート2つがクオートされている場合、
						// 「シングルクオート1文字」の文字列と見なす。
						//
						// 次に続く文字がシングルクオートの場合、バックスラッシュでクオートする。
						// （''だったら\'に変換）
						token = tokenizer.nextToken();
						if(token == '\''){
							m_sql.append("\\'");
							break;
						}
						//それ以外はトークナイザーのカレントを戻してフラグをoff（文字列のくくりを終わる）
						tokenizer.pushBack();
					}
					m_sql.append('\'');
					m_isQuoteChar = !m_isQuoteChar;//文字列判断フラグをトグル切り替え
					break;
		
				case '-':
					if(m_isQuoteChar ){
						m_sql.append((char)tokenizer.ttype); 
						break;
					}
					//次の文字がハイフンなら行コメント
					token = tokenizer.nextToken();
					if(token == '-'){
						m_lineComment = true;
					}
					else
					{
						m_sql.append('-'); 
						tokenizer.pushBack();
					}
					
					break;

				case '/':
					if(m_isQuoteChar ){
						m_sql.append((char)tokenizer.ttype); 
						break;
					}
					//次の文字が、*ならばコメントブロック
					token = tokenizer.nextToken();
					if(token == '*'){
						m_slashStarComments++;
					}else{
						m_sql.append('/'); 
						tokenizer.pushBack();
					}
					break;

				case '\\' :
					// クオートされた文字列の中にバックスラッシュがある場合
					if(m_isQuoteChar ){
						m_sql.append((char)tokenizer.ttype); 

						// 次の文字がバックスラッシュかシングルクオートだったらそのまま連結。
						// '\\' および '\'' をクオートされている文字列とみなす。
						token = tokenizer.nextToken();
						if(token == '\\' || token == '\''){
							m_sql.append((char)tokenizer.ttype);
						}else{
							tokenizer.pushBack();
						}
						break;
					}
					//バックスラッシュコマンド
					isBackSlashCmd = true;
					backSlashCmd = new StringBuffer("\\");
					break;

				default :
		
					m_sql.append((char)tokenizer.ttype); 
		
			}

		}
	}

	/**
	 * リザルトセットの内容を表示する
	 * This displays a result set.
	 * Note: it closes the result once complete.
	 * 
	 * @param rs 検索処理で返されたResultSet
	 * @throws SQLException
	 */
	public void displayResult(ResultSet rs) throws SQLException
	{
		ResultSetMetaData rsmd = rs.getMetaData();

		// Print the result column names
		int cols = rsmd.getColumnCount();
		for (int i = 1;i <= cols;i++)
			System.out.print(rsmd.getColumnLabel(i) + (i < cols ? "\t" : "\n"));
			System.out.flush();

		// now the results
		while (rs.next())
		{
			for (int i = 1;i <= cols;i++)
			{
				Object o = rs.getObject(i);
				if (rs.wasNull())
					System.out.print("{null}" + (i < cols ? "\t" : "\n"));
				else
					System.out.print(o.toString() + (i < cols ? "\t" : "\n"));
				System.out.flush();
			}
		}

		// finally close the result set
		rs.close();
	}

	/**
	 * バックスラッシュコマンド(\d、\di、\dt、\ds、\dS)を処理する
	 * This process / commands (for now just /d)
	 * 
	 * @param line コマンドラインより入力された文字列
	 * @throws SQLException
	 * 
	 */
	public void processSlashCommand(String line) throws SQLException
	{
		if (line.startsWith("\\d"))
		{

			if (line.startsWith("\\d "))
			{
				// Display details about a table
				String table = line.substring(3);
				displayResult(dbmd.getColumns(null, null, table, "%"));
			}
			else
			{
				String types[] = null;
				if (line.equals("\\d"))
					types = allUserTables;
				else if (line.equals("\\di"))
					types = usrIndices;
				else if (line.equals("\\dt"))
					types = usrTables;
				else if (line.equals("\\ds"))
					types = usrSequences;
				else if (line.equals("\\dS"))
					types = sysTables;
				else
					throw new SQLException("Unsupported \\d command: " + line);

				// Display details about all system tables
				//
				// Note: the first two arguments are ignored. To keep to the spec,
				//		 you must put null here
				//
				displayResult(dbmd.getTables(null, null, "%", types));
			}
		}
		else if (line.equals("\\q")) // Added by CWJ to permit \q command
			done = true;
		else
			throw new SQLException("Unsupported \\ command: " + line);
	}

	private static final String allUserTables[] = {"TABLE", "INDEX", "SEQUENCE"};
	private static final String usrIndices[] = {"INDEX"};
	private static final String usrTables[] = {"TABLE"};
	private static final String usrSequences[] = {"SEQUENCE"};
	private static final String sysTables[] = {"SYSTEM TABLE", "SYSTEM INDEX"};

	/**
	 * Jpsql実行時のエラー処理
	 * @deprecated COMMONS CLIにを利用するため使用しない
	 * Display some instructions on how to run the example
	 */
	public static void instructions()
	{
//		System.out.println("\nThis example shows how some of the other JDBC features work within the\ndriver. It does this by implementing a very simple psql equivalent in java.\nNot everything that psql does is implemented.\n");
//		System.out.println("Useage:\n java example.psql jdbc:postgresql:database user password [debug]\n\nThe debug field can be anything. It's presence will enable DriverManager's\ndebug trace. Unless you want to see screens of items, don't put anything in\nhere.");

		System.out.println("\n起動エラー\n	Usage :Jpsql [コンフィグレーションファイル] [SQLファイル(オプション)]\n\n");

		System.exit(1);
	}
	
	/**
	 * COMMONS CLIを利用したパースエラーに対するメッセージ出力
	 * @param options
	 */
	public static void instructions(Options options)
	{
		HelpFormatter help = new HelpFormatter();
		help.printHelp("Jpsql", options, true);
	}

	/**
	 * SQLファイルを読み込み実行する
	 * 
	 * @param filename 入力されたSQLファイル(フルパス)
	 * @throws SQLException
	 * @throws IOException
	 */
	public void ProcessSqlFile(String filename) {
		BufferedReader br;
		StringBuffer line = new StringBuffer();

		try{
			br = new BufferedReader(new FileReader(filename));
		}
		catch(FileNotFoundException e){
			System.out.println("file " + filename + " not found.");
			return;
		}

		try {
			int contents;
			while ((contents = br.read()) != -1) {
				line.append((char)contents);

				if(  /*contents == '\r' ||*/
				   contents == '\n'   ){
					
					processLine(line.toString());
						
					line = new StringBuffer();
				}
				
			}
			
			if(line.toString().trim().length() > 0){
				processLine(line.toString());
			}
		} 
		catch (IOException e) {
			System.out.println(e.getMessage());				
		}
	}


	/**
	 * This little lot starts the test
	 * 
	 * @param args[0] 接続で用いるコンフィグレーションファイルをフルパスで指定(必須項目)
	 * @param args[1] SQLの記述されたファイルをフルパスで指定(オプション)
	 */
	public static void main(String args[])
	{
		System.out.println("Welcome to Jpsql, the PostgreSQL interactive terminal. ");

		/*
		 * 
		 * if (args.length < 3)
		 *	instructions();
		 *
		 * This line outputs debug information to stderr. To enable this, simply
		 * add an extra parameter to the command line
		 * if (args.length > 3)
		 *	DriverManager.setLogStream(System.err);
		 */
		
		/*
		if(args.length != 1){
			instructions();
		}
		*/

//		/*
//		 * コマンドライン引数の数チェック
//		 * 引数の内容のチェックはJpsql()内で行う
//		 */
//		if(args.length < 1 || args.length > 2){
//			instructions();
//		}
		try
		{
			parseOptions(args);
		}
		catch (ParseException e) {
			e.getMessage();
		}
		
		// パスワードの取得(引数で指定していない場合)
		if (pwd == null) {
			PasswordCallback pass = new PasswordCallback("Password > ", false);
			Callback[] callbacks = {pass};
			InputShadowPassword cbHandler = new InputShadowPassword();
			try {
				cbHandler.handle(callbacks);
				pwd = new String(pass.getPassword());
			} catch (IOException e) {
				// TODO 自動生成された catch ブロック
				e.printStackTrace();
			} catch (UnsupportedCallbackException e) {
				// TODO 自動生成された catch ブロック
				e.printStackTrace();
			}
		}

		
		// Now run the tests
		try
		{
			Jpsql test = new Jpsql(args);
		}
		catch (Exception ex)
		{
			System.err.println("Unknown Exception caught.\n" + ex);
			ex.printStackTrace();
			System.exit(1);
		}
	}
	/**
	 * オプション定義の作成
	 * @param isRequired オプションが必須の場合:true
	 * @param hasArg オプションに値が必要な場合：true
	 * @param argName オプションの名前(Key)
	 * @param description オプションの説明
	 * @param longOpt オプションの表記名(--longOpt)
	 * @param opt オプションの表記名(-opt)
	 * @return 定義されたオプション
	 */
	private static Option makeOption(boolean isRequired, boolean hasArg, String argName, 
			String description, String longOpt, String opt)
	{
		// Builder Pattern
		Option retOption = OptionBuilder.isRequired(isRequired)
				.hasArg(hasArg)
				.withArgName(argName)
				.withDescription(description)
				.withLongOpt(longOpt)
				.create(opt);
		return retOption;
	}

	/**
	 * オプションのパース
	 * @param args 起動コマンド
	 * @throws ParseException
	 */
	private static void parseOptions(String[] args) throws ParseException
	{
			Options options = new Options();

		// オプション定義
		Option driverOption = makeOption(true, true, "driver", "PostgreSQL Driver Class Name", "driver", "d");
		Option urlOption = makeOption(true, true, "url", "JDBC url string(org.postgresql.driver", "url", "u");
		Option userOption = makeOption(true, true, "user", "DataBase user name", "user", "U");
		Option passOption = makeOption(false, true, "password", "DataBase user password", "password", "W");
		Option fileOption = makeOption(false, true, "inputfile", "SQL input file", "inputfile", "f");
		Option helpOption = makeOption(false, false, "", "help", "help", "h");

		// 定義の追加
		options.addOption(driverOption);
		options.addOption(urlOption);
		options.addOption(userOption);		
		options.addOption(passOption);		
		options.addOption(fileOption);	
		options.addOption(helpOption);
		
		// パーサー生成
		CommandLineParser parser = new GnuParser();

		// オプション解析
		CommandLine cmd = null;
		try {
			cmd = parser.parse(options, args);
			if (cmd.hasOption('h')) {
				// helpオプション
				instructions(options);
				System.exit(1);			
			}	
		} catch (ParseException e) {
			// 必須オプション未設定
			instructions(options);
			System.exit(-1);
		}

		// 解析結果の格納
		if (cmd.hasOption('d')) {
			driver = cmd.getOptionValue('d');
		}
		if (cmd.hasOption('u')) {
			url = cmd.getOptionValue('u');
		}
		if (cmd.hasOption('U')) {
			usr = cmd.getOptionValue('U');
		}
		if (cmd.hasOption('W')) {
			pwd = cmd.getOptionValue('W');
		}
		if (cmd.hasOption('f')) {
			inputfile = cmd.getOptionValue('f');
		}
	}

}

/**
 * DB接続で用いるURL、ユーザ名、パスワードを管理する
 * @deprecated コマンドラインパーサに処理を移行したため使用されない
 *  
 */
class ConnInfo{
	/** 使用するドライバクラスL */
	private String driver;
	/** 接続先DBのURL */
	private String url;
	/** 接続で用いるユーザ名 */
	private String user;
	/** 接続で用いるユーザのパスワード */
	private String password;
	
	/*
	 * 【ファイルフォーマット】
	 * 
	 * #
	 * #ファイル名	：第一引数にフルパスで指定
	 * #
	 * #以降はコメント
	 * 
	 * #使用するドライバクラス名
	 * driver=org.postgresql.Driver
	 * 
	 * #接続先DBのURLを指定
	 * url=
	 * 
	 * #接続で用いるユーザ名
	 * user=
	 * 
	 * #接続で用いるユーザのパスワード
	 * password=
	 * 
	 */
	
	/**
	 * コンフィグレーションファイルを読み込み、url、user、passwordをセットする
	 * 
	 * @param cfgfile コマンドラインから渡されるコンフィグレーションファイル名
	 *  
	 */
	protected ConnInfo(String cfgfile){
		try{
			/* コンフィグレーションファイルの読み込み */
			Properties propConn = new Properties();
			propConn.load(new FileInputStream(cfgfile));

			driver = propConn.getProperty("driver");
			url = propConn.getProperty("URL");
			user = propConn.getProperty("user");
			password = propConn.getProperty("password");
		}
		catch(Exception e){
			System.out.println("\nエラー\n	コンフィグレーションファイル読み込みエラー:" + cfgfile + "\n\n");
			e.printStackTrace();
			System.exit(1);
		}
	};

	/**
	 *  使用するドライバクラス名を返す
	 */
	public String getDriver(){
		return driver;
	};

	/**
	 *  起動時に読み込んだ接続先URLを返す
	 */
	public String getUrl(){
		return url;
	};

	/**
	 *  起動時に読み込んだユーザ名を返す
	 */
	public String getUser(){
		return user;
	};

	/**
	 *  起動時に読み込んだパスワードを返す
	 */
	public String getPass(){
		return password;
	};
}

class InputShadowPassword implements CallbackHandler{

	public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
		for (int i = 0; i < callbacks.length; i++) {
			if(callbacks[i] instanceof PasswordCallback) {
				PasswordCallback pc = (PasswordCallback)callbacks[i];
				System.out.println(pc.getPrompt());
				System.err.flush();
				pc.setPassword(readPassword(System.in));
			}
		}
	}
	
	 // Reads user password from given input stream.
	 private char[] readPassword(InputStream in) throws IOException {
		 LineNumberReader line = new LineNumberReader(new InputStreamReader(in));
		 char[] password = line.readLine().toCharArray();		 
		 return password;
	 }
}

