
/**Installer for module  */
package oracle.jaccelerator.server;

import oracle.aurora.rdbms.Schema;
import oracle.aurora.rdbms.ClassHandle;
import oracle.aurora.rdbms.Handle;
import oracle.aurora.rdbms.ResourceHandle;
import oracle.aurora.rdbms.ObjectTypeChangedException;

import oracle.sql.CHAR;

import java.io.*;
import java.util.Date;

import java.sql.*;
import oracle.jdbc.*;


/** 
 * DLL Installer
 * 
 * drop java source "oracle/jaccelerator/server/Installer";
 */
public abstract class Installer {

  /**
   * 
   * To run: Installer("project"), where project is the name of a
   * deployment jar.
   * 
   *  */
  public static void main(String argv[]) 
    throws SQLException, IOException
  {
    if (argv.length == 0) System.out.println("usage: Installer jar1 jar2 ...");
    for (int i = 0; i < argv.length; i++) System.out.println(run(argv[i]));
  }

  public static String run (String jarName)
    throws SQLException, IOException
  {

    String bundleInstallerName = 
      "oracle.aurora.deploy." +
      jarName + "_BundleInstaller";

    Class bundleInstallerClass = null;

    try {
      bundleInstallerClass = Class.forName(bundleInstallerName);
    }
    catch (Exception e) {
      throw new SQLException("failed to find the DLL bundle installer class " + 
                             bundleInstallerName + " " + e);
    }

    try {

      OracleDriver driver = new OracleDriver();
      Connection connection = driver.defaultConnection();
      Messages messages = new Messages(connection, null);
      messages.prepareInfoTables();
      Installer installer = (Installer)bundleInstallerClass.newInstance();
      String status = installer.runAndReport();
      messages.finishInfoTables();
      return status;
    }
    catch (Exception e) {
      throw new SQLException("failed to install DLLs " + bundleInstallerName + " " + e);
    }
  }

  public static void show (String s) {
    System.out.println("-- " + s);
  }

  public static boolean verbose = false;
  public static void showVerbose (String s) {
    if (verbose) show(s);
  }

  public abstract String runAndReport () throws Exception;

  static int checkClasses (Messages messages, String[] list) 
    throws SQLException
  {
    int mismatchCount = 0;
    int length = list.length;
    int i = 0; 

    while (i < length) {
      String className = list[i++];
      String schema = list[i++];
      String digest = list[i++];
      String definers = list[i++];
      ClassHandle h = Handle.lookupClass(className, Schema.lookup(schema));
      if (h == null) {
        mismatchCount++;
        messages.makeErrorRecord(className,
                                 "no such class in " + schema,
                                 new Date());
      }
      else if (digest_equal(h, digest) == false) {
        mismatchCount++;
        messages.makeErrorRecord(className,
                                 "class MD5 digest mismatch",
                                 new Date());
      }
      else if (definersStatus_equal(h, definers) == false) {
        mismatchCount++;
        messages.makeErrorRecord(className,
                                 "was definers=" + definers + ", now otherwise",
                                 new Date());
      }
    }
    return mismatchCount;
  }

  static boolean digest_equal (ClassHandle h, String digest) 
    throws SQLException
  { 
    return ClassProperties.digest(h).equals(digest);
  }


  /**
   * 
   * In many cases, this check matters for module classes
   * only. However, any code motion or inlining should preserve
   * definers semantics, and therefore we apply this test to the
   * entire fragile set.
   *
   *  */
  static boolean definersStatus_equal (ClassHandle h, String definers) 
  { 
    boolean equals = false;
    try {
      boolean isDefiners = h.definers();
      equals = 
        (definers.equals("false") && isDefiners == false)
        ||
        (definers.equals("true") && isDefiners == true)
        ;
    } catch (ObjectTypeChangedException e) {}
    return equals;
  }

  static void setAttributes (String[] list, String schema) {
    int length = list.length;
    int i = 0; 
    while (i < length) {
      String className = list[i++];
      String lib = list[i++];
      String entry = list[i++];

      ClassHandle h = Handle.lookupClass(className, Schema.lookup(schema));
      setLibName(h,lib);
      setEntry(h,entry);
      h.setNcompIsEnabled(true);
      h.setNcompIsAllowed(true);

      showVerbose("ncomp_handle: " + className + " " +
                  "dll=" + h.getNcompDllNameAsCHAR() + " " +
                  "allowed=" + h.getNcompIsAllowed() + " " +
                  "enabled=" + h.getNcompIsEnabled() + " " +
                  "lib=" + h.getNcompLibNameAsCHAR() + " " +
                  "func=" + h.getNcompFunctionNameAsCHAR());
    }
  }

  static void setLibName (ClassHandle h, String name) {
      h.setNcompLibName(new CHAR(name.getBytes(), null));
  }

  static void setEntry (ClassHandle h, String entry) {
      h.setNcompFunctionName(new CHAR(entry.getBytes(), null));
  }

  static boolean checkPlatform (String platformDescription) {
    String oracle_home = (String)System.getProperty("oracle.aurora.rdbms.oracle_home");
    if (!oracle_home.endsWith(File.separator)) oracle_home += File.separator;
    String jtc_h_fname = oracle_home + "javavm" + File.separator + "jahome" + File.separator + "jtc.h";
    String jtc_h_checksum = Dumper.parseKeywordValuePair(jtc_h_fname, "JTC_CHECKSUM_NCOMP_H");

    return 
      platformDescription.equals(System.getProperty("os.name") + " " + 
                                 System.getProperty("os.arch") + " " +
                                 jtc_h_checksum);
  }

  public static String successMessage = "installed";
  public boolean installedSuccessfully (String status) {
    return status.equals(successMessage);
  }
        
  public static String run (String packageName,
                            String schema,
                            String libraryName,
                            String platformDescription,
                            String[] fragileSetCheckList,
                            String[] moduleClassAttributes) 
    throws SQLException, IOException
  {
    return run(packageName, schema, libraryName, platformDescription, fragileSetCheckList, moduleClassAttributes, false);
  }
  
  static String deploymentDirName = "oracle/aurora/deploy/";

  public static String run (String packageName,
                            String schema,
                            String libraryName,
                            String platformDescription,
                            String[] fragileSetCheckList,
                            String[] moduleClassAttributes,
                            boolean abortIfNoResource) 
    throws SQLException, IOException
  {
    OracleDriver driver = new OracleDriver();
    Connection connection = driver.defaultConnection();
    
    Messages messages = new Messages(connection, libraryName);

    if (schema != null) schema = schema.toUpperCase();

    if (checkPlatform(platformDescription) == false) {
      show("-- wrong OS/platform/checksum: " + platformDescription);
      return report(messages, "not installed, incompatibe binaries: " + platformDescription);
    }

    ResourceHandle h = Handle.lookupResource(libraryName, Schema.lookup(schema));
    if (h == null) {
      if (abortIfNoResource) {
        return report(messages, "not installed, no such DLL:");
      }
      else {
        // report(messages, "DLL is not a resource:");
      }
    }

    int mismatchCount = checkClasses(messages, fragileSetCheckList);
    showVerbose("mismatchCount: " + mismatchCount);
    if (mismatchCount != 0) {
      if (h != null) dropResource(h);
      return 
        abortIfNoResource 
        ? report(messages, "not installed, class mismatch")
        : report(messages, "not enabled, class mismatch")
        ;
    }

    ClassProcessor disabler = new PackageDisableNcomp(packageName, schema);

    ForEachClass.inPackage(packageName, schema).apply(disabler);
    setAttributes(moduleClassAttributes, schema);

    int baseNameIdx = libraryName.lastIndexOf('/');
    String libraryBaseName = 
      (baseNameIdx < 0)
      ? libraryName
      : libraryName.substring(baseNameIdx);

    String chmod_command = (String)System.getProperty("oracle.aurora.ncomp.lib.permission");    

    String oracle_home = (String)System.getProperty("oracle.aurora.rdbms.oracle_home");
    if (!oracle_home.endsWith(File.separator)) oracle_home += File.separator;

    String libPath  = 
      oracle_home + "javavm" + File.separator + 
      "admin" + File.separator + 
      libraryBaseName;

    String result = null;

    if (h != null) {
      FileOutputStream file = Dumper.openFileOutputStream(libPath);
      Dumper.copy(h, file);
      file.close();
      showVerbose("dumped dll: " + h);
      if (chmod_command.equals("") == true) {
          showVerbose("No need to invoke chmod on:" + libPath);
      }
      else {
          showVerbose("Invoking chmod on:" +libPath);
          try {
              Process p = Runtime.getRuntime().exec(chmod_command + libPath);
              p.waitFor();
              p.destroy();
          } catch (Exception e) {
              throw new SQLException ("Error: " + e + " while trying to set +x permission on " + libPath);
          }
      }
      dropResource(h);
      result = report(messages, successMessage + " from java resource");
    }
    else {
      if (libraryName.startsWith(deploymentDirName)) {
        String deployedFileName = libraryName.substring(deploymentDirName.length());
        showVerbose("copying: " + deployedFileName);
        FileInputStream deployedFile = 
          Dumper.openFileInputStream(oracle_home + "javavm" + File.separator + 
                               "deploy" + File.separator + 
                               deployedFileName);
        FileOutputStream file = Dumper.openFileOutputStream(libPath);
        Dumper.copy(deployedFile, file);
        deployedFile.close();
        file.close();
        showVerbose("copied from deploy to admin: " + deployedFileName);
        if (chmod_command.equals("") == true) {
            showVerbose("No need to invoke chmod on:" + libPath);
        }
        else {
            try {
                showVerbose("Setting execute permission on: " + libPath);
                Process p = Runtime.getRuntime().exec(chmod_command + libPath);
                p.waitFor();
                p.destroy();
            } catch (Exception e) {
                throw new SQLException ("Error: " + e + " while trying to set +x permission on " + libPath);
            }
        }
        result = report(messages, successMessage);
      }          
      else {
        showVerbose("enabling: " + libraryBaseName);
        result = report(messages, "enabled");
      }
    }

    finishRun(connection, libraryName);
    return result;
  }

  static void finishRun (Connection connection, String libraryName) 
    throws SQLException
  {
    try {
      connection.commit();
    }
    catch (Exception e) {
      String text = "got error at commit when installing " + libraryName +  " : " + e;
      show(text);
      throw new SQLException(text);
    }
  }

  protected static void dropResource (ResourceHandle h) 
    throws SQLException
  {
    try {
      if (h != null) h.drop();
    }
    catch (Exception e) {
      throw new SQLException("dropResource exception: " + e);
    }
  }

  protected static String report (Messages messages, String message)
    throws SQLException
  {
    messages.makeInfoRecord(message, new Date());
    return message;
  }

  public static String report (String schemaAndName) 
    throws SQLException
  {
    int colonIdx = schemaAndName.indexOf(':');
    String name = schemaAndName.substring(colonIdx + 1);
    String schema = schemaAndName.substring(0, colonIdx);
    String  result;

    System.out.println("TryHandle " + name);

    ClassHandle h = Handle.lookupClass(name, Schema.lookup(schema));
    if (h == null) {
      result = "Handle " + name + " is NULL";
    }
    else {
      result = 
        ("enabled=" + h.getNcompIsEnabled() + " " +
         "allowed=" + h.getNcompIsAllowed() + " " +
         "lib=" + h.getNcompLibNameAsCHAR() + " " +
         "func=" + h.getNcompFunctionNameAsCHAR() + " " +
         "dll=" + h.getNcompDllNameAsCHAR() + " " +
         "source=" + h.getNcompSourceNameAsCHAR());
    }
    
    System.out.println("Handle " + name + " " + new Date()  + " " + result);
    return result;
  }
}

class Messages {

  public static void show (String s) {
    System.out.println(s);
  }


  public static java.text.SimpleDateFormat dateFormatter  = 
    new java.text.SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

  public static String format(Date date) {
    return dateFormatter.format(date);
  }

  Connection connection;
  String libraryName;

  public Messages (Connection connection, String libraryName) {
    this.connection = connection;
    if (libraryName != null) {
      int baseNameIdx = libraryName.lastIndexOf('/');
      libraryName = baseNameIdx > 0 ? libraryName.substring(baseNameIdx) : libraryName;
    }
    this.libraryName = libraryName;
  }

  static String libraryInfoTableName = "jaccelerator$dlls";
  static String libraryErrorsTableName = "jaccelerator$dll_errors";
  static String statusTableName = "jaccelerator$status";

  static int dll_name_length = 200;
  static int status_length = 30;
  static int timestamp_length = Messages.format(new Date()).length();
  static int problem_length = 30;
  static int class_name_length = 200;

  static boolean prepareInfoTables_done = false;

  void prepareInfoTables () 
    throws SQLException
  {
    if (!prepareInfoTables_done) {
      Statement stmt = null;
      try {
        try {
          stmt = connection.createStatement();
          String command = null;

          if (!tableIsDefined(stmt, libraryInfoTableName)) {
            String[] columns = {
              SQL.makeVarchar2Column("dll_name", dll_name_length),
              SQL.makeVarchar2Column("status", status_length),
              SQL.makeVarchar2Column("timestamp", timestamp_length)
            };
            command = "create table " + libraryInfoTableName + SQL.makeColumns(columns);
            // show("-- command: " + command);
            stmt.execute(command);
            connection.commit();
          }

          if (!tableIsDefined(stmt, libraryErrorsTableName)) {
            String[] columns = {
              SQL.makeVarchar2Column("dll_name", dll_name_length),
              SQL.makeVarchar2Column("problem", problem_length),
              SQL.makeVarchar2Column("class_name", class_name_length),
              SQL.makeVarchar2Column("timestamp", timestamp_length)
            };
            command = "create table " + libraryErrorsTableName + SQL.makeColumns(columns);
            // show("-- command: " + command);
            stmt.execute(command);
            connection.commit();
          }

          if (!tableIsDefined(stmt, statusTableName)) {
            String[] columns = {
              SQL.makeVarchar2Column("class_name", dll_name_length),
              SQL.makeVarchar2Column("status", problem_length),
              SQL.makeVarchar2Column("timestamp", timestamp_length)
            };
            command = "create table " + statusTableName + SQL.makeColumns(columns);
            // show("-- command: " + command);
            stmt.execute(command);
            connection.commit();
          }     
        }
        finally {               
          if (stmt != null) stmt.close();
        }
      }
      catch (Exception e) {
        String text = "got error at create jaccelerator tables " + e;
        show(text);
        throw new SQLException(text);
      }
      prepareInfoTables_done = true;
    }
  }

  static String truncate (String value, int length) {
    return (value.length() > length)
      ? value.substring(0, length)
      : value
      ;
  }

  void makeInfoRecord (String message, Date date) 
    throws SQLException
  {
    prepareInfoTables();
    PreparedStatement pstmt = null;
    try {
      try {
        /* First delete an existing row */
        // pstmt = connection.prepareStatement("DELETE FROM " + libraryInfoTableName + " WHERE dll_name = ? ");
        // pstmt.setString( 1, truncate(libraryName, dll_name_length));
        // pstmt.executeUpdate();
        // pstmt.close();
        /* Now insert new values */
        pstmt = connection.prepareStatement("INSERT INTO " + libraryInfoTableName + 
                                            " (dll_name, status, timestamp) VALUES(?, ?, ?)");
        int fidx = 1;
        pstmt.setString(fidx++, truncate(libraryName, dll_name_length));
        pstmt.setString(fidx++, truncate(message, status_length));
        pstmt.setString(fidx++, Messages.format(date));
        pstmt.executeUpdate();

        // connection.commit();
      } finally {
        if (pstmt != null)
          pstmt.close();
      }
    } catch (SQLException e) {
      String text = "got error at insert into jaccelerator$dlls " + e;
      show(text);
      throw new SQLException(text);
    }
  }

  void makeErrorRecord (String objectName, String message, Date date) 
    throws SQLException
  {
    prepareInfoTables();
    PreparedStatement pstmt = null;
    try {
      try {
        pstmt = connection.prepareStatement("INSERT INTO " + libraryErrorsTableName + 
                                            " (dll_name, class_name, problem, timestamp) VALUES(?, ?, ?, ?)");
        int fidx = 1;
        pstmt.setString(fidx++, truncate(libraryName, dll_name_length));
        pstmt.setString(fidx++, truncate(objectName, class_name_length));
        pstmt.setString(fidx++, truncate(message, problem_length));
        pstmt.setString(fidx++, Messages.format(date));
        pstmt.executeUpdate();

        // connection.commit();
      } finally {
        if (pstmt != null)
          pstmt.close();
      }
    } catch (SQLException e) {
      String text = "got error at insert into " + libraryErrorsTableName + "  " + e;
      show(text);
      throw new SQLException(text);
    }
  }

  void clearStatusRecords () 
    throws SQLException
  {
    prepareInfoTables();
    PreparedStatement pstmt = null;
    try {
      try {
        pstmt = connection.prepareStatement("DELETE FROM " + statusTableName + " ");
        pstmt.executeUpdate();
        pstmt.close();
        // connection.commit();
      } finally {
        if (pstmt != null)
          pstmt.close();
      }
    } catch (SQLException e) {
      String text = "got error at delete from " + statusTableName + "  " + e;
      show(text);
      throw new SQLException(text);
    }
  }

  void makeStatusRecord (String className, String status, Date date, boolean shouldDelete) 
    throws SQLException
  {
    prepareInfoTables();
    PreparedStatement pstmt = null;
    try {
      try {
        if (shouldDelete) {
          /* First delete existing rows */
          pstmt = connection.prepareStatement("DELETE FROM " + statusTableName + " WHERE class_name = ? ");
          pstmt.setString( 1, truncate(className, class_name_length));
          pstmt.executeUpdate();
          pstmt.close();
        }
        /* Now insert new values */
        pstmt = connection.prepareStatement("INSERT INTO " + statusTableName + 
                                            " (class_name, status, timestamp) VALUES(?, ?, ?)");
        int fidx = 1;
        pstmt.setString(fidx++, truncate(className, class_name_length));
        pstmt.setString(fidx++, truncate(status, status_length));
        pstmt.setString(fidx++, Messages.format(date));
        pstmt.executeUpdate();

        // connection.commit();
      } finally {
        if (pstmt != null)
          pstmt.close();
      }
    } catch (SQLException e) {
      String text = "got error at insert into " + statusTableName + "  " + e;
      show(text);
      throw new SQLException(text);
    }
  }

  public static boolean tableIsDefined (Statement stmt, String tableName) 
    throws SQLException
  {
    int counter = 0;

    try {
      String cmd;
      cmd = 
        "select table_name from user_tables where " +
        SQL.makeEqlValueExpr("table_name", tableName.toUpperCase());
      ResultSet rset = stmt.executeQuery(cmd);
      while (rset.next())
      {
        String className = rset.getString(1);
        // show("-- className: " + className);
        counter++;
      }
      return (counter == 1);
    }
    catch (SQLException e) {
      String text = "got error at select from user_tables " + e;
      show(text);
      throw new SQLException(text);
    }
  }

  void finishInfoTables () 
    throws SQLException
  {
    try {
      connection.commit();
    }
    catch (Exception e) {
      String text = "got error at commit for jaccelerator installer's tables" + e;
      show(text);
      throw new SQLException(text);
    }
  }

}


