import java.sql.*;
import java.io.*;
import uk.ac.ic.doc.automed.*;
import uk.ac.ic.doc.automed.reps.*;
import java.util.ArrayList;
import java.util.HashMap;

/*
 * An application to demonstrate how metadata my be read from 
 * a relational database that complies with entry level SQL-92,
 * and the data read used to create an AutoMed data model and
 * associated schema.
 *
 * Peter McBrien (pjm@doc.ic.ac.uk)
 */

public class ReadMetaData {
  private static int debug=0;
  private boolean createSQL92Model=false;
  private int featureSet=0;
  private static final int INDEX=1;
  private static final int PRIMARY_KEY=2;
  private static final int FOREIGN_KEY=4;
  private static final int NULL_CONSTRAINT=8;
  private static final int DATA_TYPE=16;
  private static final int DATA_SIZE=32;
  private static final int FIELD_POSITION=64;
  private String protocolName="jdbc";
  private String baseName="sql92";
  private String modelName=baseName;
  private String tableConstructName="table";
  private String fieldConstructName="field";
  private String primaryKeyConstructName="primary_key";
  private String foreignKeyConstructName="foreign_key";
  private String indexConstructName="index";
  private String user;
  private String password;
  private String url;
  private String driver="org.postgresql.Driver";
  private Connection con=null;
  private java.sql.Statement stmt=null;

  /**
   * Allows certain features of the SQL92 data model to be  represented
   * in the AutoMed repository. 
   *
   * @param features a bit wise `or' of the feature flags INDEX,
   * PRIMARY_KEY, FOREIGN_KEY, NULL_CONSTRAINT, DATA_TYPE, DATA_SIZE,
   * FIELD_POSITION, indicating which features are to be used in addition.
   * By default no features are used, maaning that the AutoMed comprises
   * of tables with untyped fields
   **/
  public void setFeatures(int features) {
    featureSet=(featureSet | features);
    setModelName();
  }

  private void setModelName() {
    if (featureSet==0)
      modelName=baseName;
    else
      modelName=baseName+"_"+featureSet;
  }

  public String getModelName() {
    return modelName;
  }

  public int getFeatures() {
    return featureSet;
  }

  /**
   * Test if a certain group of features is being implemented
   * in the representation
   * of the SQL92 data model in the AutoMed repository
   *
   * @param features is the bitwise `or' of feature ignore flags to be
   * tested
   *
   * @return true if all those features are being ignored
   */
  public boolean isFeatureInUse(int features) {
    return ((featureSet & features)==features);
  }

  public void setDebug(int debug) {
    this.debug=debug;
  }

  public void setURL(String url) {
    this.url=url;
  }

  public void setDriver(String driver) {
    this.driver=driver;
  }

  public void setUser(String user) {
    this.user=user;
  }

  public void setPassword(String password) {
    this.password=password;
  }

  /** Set what name is used in the AutoMed repository for the modelling
   *  language in which SQL92 is to be represented. Note that if any
   *  features are set, then the actual name created with be suffixed with
   *  an underscore and the numeric value returned by {@link #getFeatures}.
   *  Hence one may set different features for different imports of the
   *  relational model, and have them stored under different model names
   */
  public void setBaseName(String baseName) {
    this.baseName=baseName;
    setModelName();
  }

  /*
   * Define a relational modelling language for the SQL92 standard, together
   * with the JDBC protocol for accessing SQL92 compliant databases.
   */
  public void createSQL92Model(boolean force) {
    try { 
      // Zap any existing model called modelName
      if (Model.exists(modelName) && force) {
	//	STR.setDebugLevel(4);
	Model.getModel(modelName).retract();
	//	STR.setDebugLevel(0);
      }

      Model rel=Model.createModel(modelName); 

      // TABLE
      Construct table=
	rel.createConstruct(tableConstructName,
			    Construct.CLASS_NODAL,
			    true);
      table.addNodeNameScheme();
			       
      // FIELD
      Construct field=
	rel.createConstruct(fieldConstructName,
			    Construct.CLASS_LINK_NODAL,
			    true);
      field.addReferenceScheme(table);
      field.addEdgeNameScheme();
      if (isFeatureInUse(NULL_CONSTRAINT))
	field.addLabelScheme(); // To hold null or notnull
      if (isFeatureInUse(DATA_TYPE)) 
	field.addLabelScheme(); // To hold datatype: int,string,...
      if (isFeatureInUse(DATA_SIZE)) 
	field.addLabelScheme(); // To hold datatype size:  length of string,...
      if (isFeatureInUse(FIELD_POSITION)) 
	field.addLabelScheme(); // To hold position in table: 1,2,3,...

      // PRIMARY KEY
      if (isFeatureInUse(PRIMARY_KEY)) {
	Construct primary_key=
	  rel.createConstruct(primaryKeyConstructName,
			      Construct.CLASS_CONSTRAINT,
			      true);
	primary_key.addLabelScheme(1, 1, true);
	primary_key.addReferenceScheme(table, 1, 1, true);
	primary_key.addReferenceScheme(field, 1, 0, true);
      }

      // INDEX
      if (isFeatureInUse(INDEX)) {
	Construct index=
	  rel.createConstruct(indexConstructName,
			      Construct.CLASS_CONSTRAINT,
			      true);
	index.addLabelScheme(1, 1, true); // Name of index
	index.addLabelScheme(1, 1, true); // unique or non_unique
	index.addLabelScheme(1, 1, true); // asc or desc
	index.addReferenceScheme(field, 1, 0, true);
      }

      // FOREIGN KEY
      if (isFeatureInUse(FOREIGN_KEY)) {
	Construct foreign_key=
	  rel.createConstruct(foreignKeyConstructName,
			      Construct.CLASS_CONSTRAINT,
			      true);
	foreign_key.addLabelScheme(1, 1, true);
	foreign_key.addReferenceScheme(table, 1, 1, true);
	foreign_key.addReferenceScheme(field, 1, 0, true);
	foreign_key.addReferenceScheme(table, 1, 1, true);
	foreign_key.addReferenceScheme(field, 1, 0, true);
      }

      System.out.println("Created "+modelName+" Model");
    }
    catch (Exception e) {
      throw new InconsistentException("Failed to create the sql92 model",e);
    }
  }

  public Schema createSchemaFromMetaData(String schemaName) {
    try {
      Model rel=Model.getModel(modelName);
      Construct table=rel.getConstruct(tableConstructName);
      Construct field=rel.getConstruct(fieldConstructName);
      Construct pk=(isFeatureInUse(PRIMARY_KEY) ? 
		     rel.getConstruct(primaryKeyConstructName) : null);
      Construct fk=(isFeatureInUse(FOREIGN_KEY) ? 
		     rel.getConstruct(foreignKeyConstructName) : null);
      Construct idx=(isFeatureInUse(INDEX) ? 
		     rel.getConstruct(indexConstructName) : null);
      Protocol protocolJDBC;

      // If there is no JDBC protocol, create it
      if (!Protocol.exists(protocolName))
	protocolJDBC=Protocol.createProtocol(protocolName);
      else
        protocolJDBC=Protocol.getProtocol(protocolName);

      // We will go for double angle brackets to represent the chevrons in
      // this application
      Schema.setOpenCloseSchemeBracket("<<",">>");

      // force the creatation of a new schema called schemaName
      Schema.retract(schemaName);
      Schema s=Schema.createSchema(schemaName,false);

      try {
	Class.forName(driver).newInstance();
      }
      catch (ClassNotFoundException cnfe) {
	System.out.println("Could not find the class for "+driver+"; "+
			   "please ensure your CLASSPATH is set correctly");
	return null;
      }
      catch (Exception e) {
	System.out.println("Error in loading the driver "+driver);
	e.printStackTrace();
      }

      if (debug>=5)
	System.out.println("Connecting to "+url+
			   " as "+user+
			   " password="+password);
      con=DriverManager.getConnection(url,user,password);
      stmt=con.createStatement();

      // Pickup details of the relational model we will use
      DatabaseMetaData md=con.getMetaData();
      /* A map of tableName --> SchemaObject and
       * fieldName --> SchemaObject
       */
      HashMap newSchemaObjects=new HashMap();
      ResultSet rs=md.getTables(null,null,"%",new String[]{"TABLE"});

      while (rs.next()) {
	String tableName=rs.getString(3);
	if (debug>1)
	  System.out.println("Reading data for "+tableName);
	SchemaObject tableInstance=
	  s.createSchemaObject(table,new Object[]{tableName});
	newSchemaObjects.put(tableName,tableInstance);
	

	// Create the fields of this table
	//ResultSet rsCol=md.getColumns(null,null,tableName+"%","%");
	ResultSet rsCol=md.getColumns(null,null,tableName,"%");
	
	// The JDBC documentation appears to say that both column 4 and
	// column 18 indicate the null/notnull status of a field. At least
	// for the Sybase driver, the column 4 always returns unknown, whilst
	// column 18 is YES/NO, so I look at both here!
	while (rsCol.next()) {
	  String fieldName=rsCol.getString(4);
	  String nullable=null;

          switch (rsCol.getInt(11)) {
	  case ResultSetMetaData.columnNoNulls :
	    nullable="notnull";
	    break;
	  case ResultSetMetaData.columnNullable :
	    nullable="null";
	    break;
	  }

          if (nullable==null) {
	    String sn=rsCol.getString(18);
	    if ("NO".equals(sn))
	      nullable="notnull";
	    else 
	      if ("YES".equals(sn))
		nullable="null";
	      else
		nullable="unknown";
	  }

	  // Things like text have no limit on there size, which we will
	  // model as -1
	  String fieldSize=rsCol.getString(8);
	  if (fieldSize==null) fieldSize="-1";

          if (debug>9)
	    for (int j=1;j<19;j++)
	      System.out.println("MD["+j+"]="+rsCol.getObject(j));

	  if (debug>5)
	    System.out.println("Making "+tableInstance+","+
			       fieldName+","+
			       nullable+","+
			       rsCol.getString(6)+","+
			       fieldSize+","+
			       rsCol.getString(17));

	  ArrayList fieldScheme=new ArrayList();
	  fieldScheme.add(tableInstance);
	  fieldScheme.add(fieldName);
	  if (isFeatureInUse(NULL_CONSTRAINT)) 
	  fieldScheme.add(nullable);
	  if (isFeatureInUse(DATA_TYPE)) 
	  fieldScheme.add(rsCol.getString(6));
	  if (isFeatureInUse(DATA_SIZE)) fieldScheme.add(fieldSize);
	  if (isFeatureInUse(FIELD_POSITION)) 
	  fieldScheme.add(rsCol.getString(17));

	  SchemaObject fieldInstance=
	  s.createSchemaObject(field,fieldScheme.toArray());

	  newSchemaObjects.put(tableName+"."+fieldName,fieldInstance);
	}

	// Read in the primary key (PK) 
	if (isFeatureInUse(PRIMARY_KEY)) {
	  ResultSet rsPK=md.getPrimaryKeys(null,null,tableName);
	  ArrayList pkFields=new ArrayList();
	  String pkName=null;

	  pkFields.add(pkName);
	  pkFields.add(tableInstance);

	  while (rsPK.next()) {
	    String pkFieldName=rsPK.getString(3)+"."+rsPK.getString(4);
	    int pos=rsPK.getInt(5)+1; // KEY_SEQ=1 goto index 2, ... 

	    pkName=rsPK.getString(6);

	    // Add the new element, padding out array if necessary
	    while (pkFields.size()<pos+1)
	      pkFields.add(null);
	    pkFields.set(pos,newSchemaObjects.get(pkFieldName));
	  }

	  // If we don't have a pk name in the DB, generate one
	  if (pkName==null) 
	    pkName=tableName+"_pk";

	  pkFields.set(0,pkName);

	  // Check that there is a PK before creating it
	  if (pkFields.size()>2)
	    s.createSchemaObject(pk,pkFields.toArray());

	  // Now look at indexes on the table
	  // Not sure about the final "true" (means eliminate approximate 
	  // values); sounds like something to deal with a flaw in a major
	  // database vendor's implementation rather than a true JDBC feature
	  if (isFeatureInUse(INDEX)) {
	    ResultSet rsIDX=md.getIndexInfo(null,null,tableName,false,true);
	    HashMap allIDXs=new HashMap();

	    while (rsIDX.next()) {
	      String IDXName=rsIDX.getString(6);	  
	      String fieldName=rsIDX.getString(3)+"."+rsIDX.getString(9);
	      int pos=rsIDX.getInt(8)+2; // ORDINAL_POSITION=1 goto index 3, ... 
	    
	      ArrayList IDXList;
	    
	      // Don't care about nameless indexes, such as those that 
	      // are used for statistical purposes within the db
	      if (IDXName==null) continue;
	    
	      // Have we seen part of this index before ...
	      if (allIDXs.containsKey(IDXName)) {
		IDXList=(ArrayList)allIDXs.get(IDXName);
	      } 
	      else {
		IDXList=new ArrayList();
		IDXList.add(IDXName);    
	      
		String non_unique=rsIDX.getBoolean(4) ? "non_unique" : "unique";
	      
		String asc_or_desc=rsIDX.getString(10);
		if ("D".equals(asc_or_desc)) asc_or_desc="desc";
		if ("A".equals(asc_or_desc)) asc_or_desc="asc";
		if (asc_or_desc==null) asc_or_desc="none";
	      
		IDXList.add(non_unique);
		IDXList.add(asc_or_desc);
		allIDXs.put(IDXName,IDXList);
	      }
	    
	      // Add the new element, padding out array if necessary
	      while (IDXList.size()<pos+1) 
		IDXList.add(null);
	    
	      IDXList.set(pos,newSchemaObjects.get(fieldName));
	    }
	  
	    Object[] idxlist=allIDXs.keySet().toArray();
	  
	    for (int i=0;i<idxlist.length;i++) {
	      ArrayList idxscheme=(ArrayList)allIDXs.get(idxlist[i]);
	      s.createSchemaObject(idx,idxscheme.toArray());
	    }
	  }
	}
      }

      // Now that all tables are created, create foreign keys (FK) between them
      if (isFeatureInUse(FOREIGN_KEY)) {
	rs=md.getTables(null,null,"%",new String[]{"TABLE"});
	
	while (rs.next()) {
	  HashMap allFKs=new HashMap();
	  HashMap allFKPKs=new HashMap();
	  String tableName=rs.getString(3);
	  
	  // Read in the foreign keys (FK) 
	  ResultSet rsFK=md.getExportedKeys(null,null,tableName);
	  ArrayList fkFields;
	  
	  while (rsFK.next()) {
	    String fkName=rsFK.getString(12);	  
	    String pkFieldName=rsFK.getString(3)+"."+rsFK.getString(4);
	    String fkFieldName=rsFK.getString(7)+"."+rsFK.getString(8);
	    int pos=rsFK.getInt(9)+1; // KEY_SEQ=1 goto index 2, ... 
	    
	    if (fkName==null)
	    fkName=tableName+"_fk";
	    
	    ArrayList FKList;
	    ArrayList PKList;
	    
	    // Have we seen part of this FK before ...
	    if (allFKs.containsKey(fkName)) {
	      FKList=(ArrayList)allFKs.get(fkName);
	      PKList=(ArrayList)allFKPKs.get(fkName);
	    } 
	    else {
	      FKList=new ArrayList();
	      FKList.add(fkName);
	      PKList=(ArrayList)FKList.clone();
	      FKList.add(newSchemaObjects.get(rsFK.getString(7)));
	      allFKs.put(fkName,FKList);
	      PKList.add(newSchemaObjects.get(rsFK.getString(3)));
	      allFKPKs.put(fkName,PKList);
	    }
	    
	    FKList.add(newSchemaObjects.get(fkFieldName));
	    PKList.add(newSchemaObjects.get(pkFieldName));	  
	  }
	  
	  Object[] fklist=allFKs.keySet().toArray();

	  for (int i=0;i<fklist.length;i++) {
	    ArrayList fkscheme=(ArrayList)allFKs.get(fklist[i]);
	    ArrayList pkscheme=(ArrayList)allFKPKs.get(fklist[i]);
	    pkscheme.remove(0);
	    fkscheme.addAll(pkscheme);
	    Object a[]=fkscheme.toArray();
	    
	    if (debug>5)
	    for (int j=0;j<a.length;j++)
	    System.out.println("FK["+j+"]="+a[j]);
	    
	    s.createSchemaObject(fk,fkscheme.toArray());
	  }
	}
      }

      stmt.close();
      con.close();

      // Describe this new access method
      AccessMethod.createAccessMethod(s,url,protocolJDBC,driver,user);
      return s;
    }
    catch (SQLException se) { 
      se.printStackTrace();
    }
    catch (AutoMedException ame) {
      ame.printStackTrace();
    }

    return null;
  }

  public static void printMetaData(Connection con) 
  throws SQLException, IOException {
    DatabaseMetaData md=con.getMetaData();

    printResultSet(md.getSchemas(),"getSchemas");

    printResultSet(md.getTableTypes(),"getTableTypes");

    // Use new String[]{"TABLE"} as 4th parameter to get just
    // user defined tables , use null to get all objects
    printResultSet(md.getTables(null,null,"%",new String[]{"TABLE"}),"getTables");

    ResultSet rs=md.getTables(null,null,"%",new String[]{"TABLE"});

    while (rs.next()) {
      String table=rs.getString(3);
      System.out.println("*** Details for table "+table+"\n");

      printResultSet(md.getImportedKeys(null,null,table),"getImportedKeys");

      printResultSet(md.getExportedKeys(null,null,table),"getExportedKeys");

      printResultSet(md.getPrimaryKeys(null,null,table),"getPrimaryKeys");

      printResultSet(md.getIndexInfo(null,null,table,false,false),"getIndexInfo");

      printResultSet(md.getColumns(null,null,table+"%","%"),"getColumns");
    }
  }

  public static void printResultSet(ResultSet rs,String tableName) 
  throws SQLException{
    if (rs==null) {
      System.out.println("Contents of "+tableName+" give null result set");
      return;
    }

    ResultSetMetaData rsmd=rs.getMetaData();
    int noc=rsmd.getColumnCount();

    System.out.println("Contents of "+tableName);

    for (int i=1;i<=noc;i++) {
      if (i!=1) System.out.print(",");
      System.out.print(rsmd.getColumnName(i));
    }

    System.out.println();
			 
    while (rs.next()) {
      for (int i=1;i<=noc;i++) {
	if (i!=1) System.out.print(",");
	System.out.print(rs.getObject(i));
      }
      System.out.println();
    }
			 
    System.out.println("\n");
  }

  public static void main(String[] args) {
    boolean recreateSQL92Model=false;

    System.out.println("AutoMed Repository: "+
		       "Creation of Schemas from JDBC MetaData");

    // level 5 prints all the old debug information, 0 makes it silent
    STR.setDebugLevel(0);

    // We will go for double angle brackets to represent the chevrons in
    // this application
    Schema.setOpenCloseSchemeBracket("<<",">>");

    ReadMetaData rmd=new ReadMetaData();
    // String schemaName="";

    if (args.length == 0) {
      System.out.println("You didn't specify any arguments!");
      printHelp();
      System.exit(10);
    } 
    else {
      int i=0;

      try {
	while (i<args.length) {     	
	  if (args[i].charAt(0)!='-') {
	    System.out.println("Error at "+args[i]+"\n");
	    printHelp();
	    System.exit(10);
	  } 
	  else if (args[i].startsWith("-setFeatures")) 
	    rmd.setFeatures(Integer.parseInt(args[++i]));
	  else if (args[i].startsWith("-recreateSQL92Model")) { 
	    try {
	      rmd.createSQL92Model(true);
	    }
	    catch (Exception e) {
	      System.out.println("Execution halting since "+rmd.getModelName()+
				 " datamodel cannot be created in repository");
	      e.printStackTrace();
	      System.exit(10);
	    }
	  }
	  else if (args[i].startsWith("-schema")) {
	    // Need SQL datamodel amd JDBC protocol
	    try {
	      if (!Model.exists(rmd.getModelName()))
		rmd.createSQL92Model(true);
	    }
	    catch (Exception e) {
	      System.out.println("Execution halting since "+rmd.getModelName()+
				 " datamodel cannot be created in repository");
	      e.printStackTrace();
	      System.exit(10);
	    }

	    // Create the schema as requested
	    Schema s=rmd.createSchemaFromMetaData(args[++i]);
	    
	    // If in debug mode printout what we have created
	    if (debug>=1) {
	      System.out.println("Created schema:");
	      try {
		s.write();
	      }
	      catch (Exception e) {
		e.printStackTrace();
	      }
	    }
	  }
	  else if (args[i].startsWith("-debug")) 
	    debug=Integer.parseInt(args[++i]);
	  else if (args[i].startsWith("-url")) 
	    rmd.setURL(args[++i]);
	  else if (args[i].startsWith("-driver")) 
	    rmd.setDriver(args[++i]);
	  else if (args[i].startsWith("-user")) 
	    rmd.setUser(args[++i]);
	  else if (args[i].startsWith("-password")) 
	    rmd.setPassword(args[++i]);
	      
	  ++i;
	}

        if (debug>=5)
	  System.out.println("Finished reading arguments, "+
			     "about to read database");


      }
      catch (ArrayIndexOutOfBoundsException e) {
	System.out.println("You didn't supply an argument to the "
			   +args[i-1]+" option!");
	printHelp();
	System.exit(10);
      }
    }
  }

  static void printHelp() {
    System.out.println("Usage: ReadMetaData [ options ]\n"+
      "  -debug <0-10>         set debug level\n" +
      "  -schema <name>        name of schema to create\n" +
      "  -url <dbURL>          URL where DB located\n" +
      "  -driver <jdbcDriverClass>\n" +
      "  -user <user>\n"+
      "  -password <password>\n"+
      "  -recreateSQL92Model   Recreate the SQL92 model\n"+
      "  -setFeatures <0-127>  Control features of SQL92 models");  
  }
}
