/*

   Derby - Class org.apache.derby.impl.tools.dblook.DB_GrantRevoke

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to you under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */

package org.apache.derby.impl.tools.dblook;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;

import java.util.StringTokenizer;

import org.apache.derby.tools.dblook;

public class DB_GrantRevoke {

	/** ************************************************
	 * Generate Grant and Revoke statements if sqlAuthorization is on
	 * 
	 * @param conn Connection to use
	 * @param at10_6 True if the database level is 10.6 or higher
	 */
	public static void doAuthorizations(Connection conn, boolean at10_6)
		throws SQLException {

		// First generate table privilege statements
		Statement stmt = conn.createStatement();
        ResultSet rs;

        if ( at10_6 )
        {
            // Generate udt privilege statements
            rs = stmt.executeQuery("SELECT P.GRANTEE, S.SCHEMANAME, A.ALIAS, P.PERMISSION, P.OBJECTTYPE FROM " +
                                   "SYS.SYSPERMS P, SYS.SYSALIASES A, SYS.SYSSCHEMAS S WHERE A.SCHEMAID = " +
                                   "S.SCHEMAID AND P.OBJECTID = A.ALIASID AND A.ALIASTYPE='A'");
            generateUDTPrivs(rs);
            
            // Generate sequence privilege statements
            rs = stmt.executeQuery("SELECT P.GRANTEE, S.SCHEMANAME, SEQ.SEQUENCENAME, P.PERMISSION, P.OBJECTTYPE FROM " +
                                   "SYS.SYSPERMS P, SYS.SYSSEQUENCES SEQ, SYS.SYSSCHEMAS S WHERE SEQ.SCHEMAID = " +
                                   "S.SCHEMAID AND P.OBJECTID = SEQ.SEQUENCEID");
            generateSequencePrivs(rs);
            
            // Generate aggregate privilege statements
            rs = stmt.executeQuery("SELECT P.GRANTEE, S.SCHEMANAME, A.ALIAS, P.PERMISSION, P.OBJECTTYPE FROM " +
                                   "SYS.SYSPERMS P, SYS.SYSALIASES A, SYS.SYSSCHEMAS S WHERE A.SCHEMAID = " +
                                   "S.SCHEMAID AND P.OBJECTID = A.ALIASID AND A.ALIASTYPE='G'");
            generateAggregatePrivs(rs);
        }

        rs = stmt.executeQuery("SELECT GRANTEE, SCHEMANAME, TABLENAME, SELECTPRIV, " +
			"DELETEPRIV, INSERTPRIV, UPDATEPRIV, REFERENCESPRIV, TRIGGERPRIV FROM " +
			"SYS.SYSTABLEPERMS P, SYS.SYSTABLES T, SYS.SYSSCHEMAS S WHERE T.SCHEMAID = " +
			"S.SCHEMAID AND T.TABLEID = P.TABLEID");
		generateTablePrivs(rs);

		// Generate column privilege statements
		rs = stmt.executeQuery("SELECT GRANTEE, SCHEMANAME, TABLENAME, TYPE, COLUMNS FROM " +
			"SYS.SYSCOLPERMS P, SYS.SYSTABLES T, SYS.SYSSCHEMAS S WHERE T.SCHEMAID = " +
			"S.SCHEMAID AND T.TABLEID = P.TABLEID");
		generateColumnPrivs(rs, conn);

		// Generate routine privilege statements
		rs = stmt.executeQuery("SELECT GRANTEE, SCHEMANAME, ALIAS, ALIASTYPE FROM " +
			"SYS.SYSROUTINEPERMS P, SYS.SYSALIASES A, SYS.SYSSCHEMAS S WHERE A.SCHEMAID = " +
			"S.SCHEMAID AND P.ALIASID = A.ALIASID");
		generateRoutinePrivs(rs);

		rs.close();
		stmt.close();
		return;

	}

	/** ************************************************
	 * Generate table privilege statements
	 * 
	 * @param rs Result set holding required information
	 ****/
	private static void generateTablePrivs(ResultSet rs)
		throws SQLException
	{
		boolean firstTime = true;
		while (rs.next()) {

			if (firstTime) {
				Logs.reportString("----------------------------------------------");
				Logs.reportMessage( "DBLOOK_TablePrivHeader");
				Logs.reportString("----------------------------------------------\n");
			}

			String authName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(1)));
			String schemaName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(2)));
			String tableName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(3)));
			String fullName = schemaName + "." + tableName;

			if (dblook.isIgnorableSchema(schemaName))
				continue;

			Logs.writeToNewDDL(tablePrivStatement(rs, fullName, authName));
			Logs.writeStmtEndToNewDDL();
			Logs.writeNewlineToNewDDL();
			firstTime = false;
		}
	}

	private static String separatorStr(boolean addSeparator)
	{
		return (addSeparator) ? ", " : "";
	}
	
	/** **************************************************
	 * Generate table privilege statement for the current row
	 *
	 * @param rs 		ResultSet holding tableperm information
	 * @param fullName	Table's qualified name
	 * @param authName	Authorization id for grant statement
	 */
	private static String tablePrivStatement(ResultSet rs, String fullName, String authName)
		throws SQLException
	{
		boolean addSeparator = false;
		StringBuffer grantStmt = new StringBuffer("GRANT ");

		if (rs.getString(4).toUpperCase().equals("Y")) 
		{
			grantStmt.append("SELECT");
			addSeparator = true;
		}

		if (rs.getString(5).toUpperCase().equals("Y"))
		{
			grantStmt.append(separatorStr(addSeparator)+ "DELETE");
			addSeparator = true;
		}

		if (rs.getString(6).toUpperCase().equals("Y"))
		{
			grantStmt.append(separatorStr(addSeparator)+ "INSERT");
			addSeparator = true;
		}

		if (rs.getString(7).toUpperCase().equals("Y"))
		{
			grantStmt.append(separatorStr(addSeparator)+ "UPDATE");
			addSeparator = true;
		}

		if (rs.getString(8).toUpperCase().equals("Y"))
		{
			grantStmt.append(separatorStr(addSeparator)+ "REFERENCES");
			addSeparator = true;
		}

		if (rs.getString(9).toUpperCase().equals("Y"))
		{
			grantStmt.append(separatorStr(addSeparator)+ "TRIGGER");
			addSeparator = true;
		}

		grantStmt.append(" ON " + fullName + " TO " + authName);

		return grantStmt.toString();
	}

	/** ************************************************
	 * Generate column privilege statements
	 * 
	 * @param rs	ResultSet holding column privilege information
	 * @param conn	Connection to use. Used to get another ResultSet
	 ****/

	private static void generateColumnPrivs(ResultSet rs, Connection conn)
		throws SQLException
	{
        // Statement that gets the names of the columns in a given table.
        PreparedStatement columnStmt = conn.prepareStatement(
            "SELECT COLUMNNUMBER, COLUMNNAME " +
            "FROM SYS.SYSCOLUMNS C, SYS.SYSTABLES T, SYS.SYSSCHEMAS S " +
            "WHERE T.TABLEID = C.REFERENCEID and S.SCHEMAID = T.SCHEMAID " +
            "AND S.SCHEMANAME = ? AND T.TABLENAME = ? " +
            "ORDER BY COLUMNNUMBER");

		boolean firstTime = true;
		while (rs.next()) {
			if (firstTime) {
				Logs.reportString("----------------------------------------------");
				Logs.reportMessage( "DBLOOK_ColumnPrivHeader");
				Logs.reportString("----------------------------------------------\n");
			}

            // Auth name will added directly to the generated DDL, so we need
            // to quote it.
			String authName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(1)));

            // Schema name and table name are parameters to a prepared
            // statement, so quoting is not needed.
            String schemaName = rs.getString(2);
            String tableName = rs.getString(3);

            // isIgnorableSchema, on the other hand, expects the schema name
            // to be quoted.
            String schemaNameQuoted =
                    dblook.addQuotes(dblook.expandDoubleQuotes(schemaName));
            if (dblook.isIgnorableSchema(schemaNameQuoted)) {
				continue;
            }

			// Create another resultSet to get column names
            columnStmt.setString(1, schemaName);
            columnStmt.setString(2, tableName);
            ResultSet rsCols = columnStmt.executeQuery();

            // The full name will be added directly to the generated GRANT
            // statement, so it needs to be quoted.
            String fullName = schemaNameQuoted + "." +
                    dblook.addQuotes(dblook.expandDoubleQuotes(tableName));

			Logs.writeToNewDDL(columnPrivStatement(rs, fullName, authName, rsCols));
			Logs.writeStmtEndToNewDDL();
			Logs.writeNewlineToNewDDL();
			firstTime = false;

            rsCols.close();
		}

        columnStmt.close();
	}

	private static String privTypeToString(String privType)
	{
		if (privType.equals("S"))
			return "SELECT";
		else if (privType.equals("R"))
			return "REFERENCES";
		else if (privType.equals("U"))
			return "UPDATE";

		// Should throw an exception?
		return "";
	}

	/** ************************************************
	 * Generate one column grant statement
	 * 
	 * @param columns	List of columns to grant required privs
	 * @param rsCols	ResultSet for mapping column numbers to names
	 ****/

	private static String mapColumnsToNames(String columns, ResultSet rsCols)
		throws SQLException
	{
		StringBuffer colNames = new StringBuffer();
		rsCols.next();
		int curColumn = 1;
		boolean addSeparator = false;

		// Strip out outer {} in addition to spaces and comma
		StringTokenizer st = new StringTokenizer(columns, " ,{}");
		while (st.hasMoreTokens())
		{
			int colNum = Integer.parseInt(st.nextToken());
			while (colNum+1 > curColumn)
			{
				rsCols.next();
				curColumn = rsCols.getInt(1);
			}
			colNames.append(separatorStr(addSeparator));
			addSeparator = true;

            String colName = dblook.addQuotes(
                    dblook.expandDoubleQuotes(rsCols.getString(2)));
            colNames.append(colName);
		}

		return colNames.toString();
	}

	/** ************************************************
	 * 
	 * @param rs		ResultSet with info for this GRANT statement
	 * @param fullName	Full qualified name of the table
	 * @param authName	Authorization name for this GRANT
	 * @param rsCols	ResultSet for mapping column numbers to names
	 ****/

	private static String columnPrivStatement(ResultSet rs, String fullName,
			String authName, ResultSet rsCols) throws SQLException
	{
		StringBuffer grantStmt = new StringBuffer("GRANT ");

		String privType = rs.getString(4).toUpperCase();
		String columns = rs.getString(5);
		grantStmt.append(privTypeToString(privType));
		grantStmt.append("(");
		grantStmt.append(mapColumnsToNames(columns, rsCols));
        grantStmt.append(") ON ");
        grantStmt.append(fullName);
		grantStmt.append(" TO ");
		grantStmt.append(authName);

		return grantStmt.toString();
	}

	/** ************************************************
	 * Generate udt privilege statements
	 *
	 * @param rs ResultSet holding required information
	 ****/
	public static void generateUDTPrivs(ResultSet rs) throws SQLException
	{
		boolean firstTime = true;
		while (rs.next()) {
			String authName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(1)));
			String schemaName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(2)));
			String aliasName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(3)));
			String fullName = schemaName + "." + aliasName;
			String permission = rs.getString(4);
			String objectType = rs.getString(5);

			if (dblook.isIgnorableSchema(schemaName))
				continue;

			if (firstTime) {
				Logs.reportString("----------------------------------------------");
				Logs.reportMessage("DBLOOK_UDTPrivHeader");
				Logs.reportString("----------------------------------------------\n");
			}

			Logs.writeToNewDDL(genericPrivStatement(fullName, authName, permission, objectType ));
			Logs.writeStmtEndToNewDDL();
			Logs.writeNewlineToNewDDL();
			firstTime = false;
		}
	}
	/** ************************************************
	 * Generate sequence privilege statements
	 *
	 * @param rs ResultSet holding required information
	 ****/
	public static void generateSequencePrivs(ResultSet rs) throws SQLException
	{
		boolean firstTime = true;
		while (rs.next()) {
			String authName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(1)));
			String schemaName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(2)));
			String sequenceName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(3)));
			String fullName = schemaName + "." + sequenceName;
			String permission = rs.getString(4);
			String objectType = rs.getString(5);

			if (dblook.isIgnorableSchema(schemaName))
				continue;

			if (firstTime) {
				Logs.reportString("----------------------------------------------");
				Logs.reportMessage("DBLOOK_SequencePrivHeader");
				Logs.reportString("----------------------------------------------\n");
			}

			Logs.writeToNewDDL(genericPrivStatement(fullName, authName, permission, objectType ));
			Logs.writeStmtEndToNewDDL();
			Logs.writeNewlineToNewDDL();
			firstTime = false;
		}
	}
	/** ************************************************
	 * Generate aggregate privilege statements
	 *
	 * @param rs ResultSet holding required information
	 ****/
	public static void generateAggregatePrivs(ResultSet rs) throws SQLException
	{
		boolean firstTime = true;
		while (rs.next()) {
			String authName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(1)));
			String schemaName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(2)));
			String aliasName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(3)));
			String fullName = schemaName + "." + aliasName;
			String permission = rs.getString(4);
			String objectType = rs.getString(5);

			if (dblook.isIgnorableSchema(schemaName))
				continue;

			if (firstTime) {
				Logs.reportString("----------------------------------------------");
				Logs.reportMessage("DBLOOK_AggregatePrivHeader");
				Logs.reportString("----------------------------------------------\n");
			}

			Logs.writeToNewDDL(genericPrivStatement(fullName, authName, permission, objectType ));
			Logs.writeStmtEndToNewDDL();
			Logs.writeNewlineToNewDDL();
			firstTime = false;
		}
	}
	private static String genericPrivStatement(String fullName, String authName, String permission, String objectType )
		throws SQLException
	{
		boolean addSeparator = false;
		StringBuffer grantStmt = new StringBuffer("GRANT " + permission + " ON " + objectType + " " );

		grantStmt.append(fullName);
		grantStmt.append(" TO ");
		grantStmt.append(authName);

		return grantStmt.toString();
	}

	/** ************************************************
	 * Generate routine privilege statements
	 *
	 * @param rs ResultSet holding required information
	 ****/
	public static void generateRoutinePrivs(ResultSet rs) throws SQLException
	{
		boolean firstTime = true;
		while (rs.next()) {
			String authName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(1)));
			String schemaName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(2)));
			String aliasName = dblook.addQuotes
				(dblook.expandDoubleQuotes(rs.getString(3)));
			String fullName = schemaName + "." + aliasName;
			String aliasType = rs.getString(4);

			if (dblook.isIgnorableSchema(schemaName))
				continue;

			// Ignore SYSCS_UTIL privileges as all new databases automatically get them
			if (schemaName.equals("\"SYSCS_UTIL\""))
				continue;

			if (firstTime) {
				Logs.reportString("----------------------------------------------");
				Logs.reportMessage("DBLOOK_RoutinePrivHeader");
				Logs.reportString("----------------------------------------------\n");
			}

			Logs.writeToNewDDL(routinePrivStatement(fullName, authName, aliasType));
			Logs.writeStmtEndToNewDDL();
			Logs.writeNewlineToNewDDL();
			firstTime = false;
		}
	}

	private static String routinePrivStatement(String fullName, String authName, String aliasType)
		throws SQLException
	{
		boolean addSeparator = false;
		StringBuffer grantStmt = new StringBuffer("GRANT EXECUTE ON ");

		grantStmt.append((aliasType.equals("P")) ? "PROCEDURE " : "FUNCTION ");
		grantStmt.append(fullName);
		grantStmt.append(" TO ");
		grantStmt.append(authName);

		return grantStmt.toString();
	}
}
