DBVIEW
src/org/dbview/input_addons/AbstractLoader.java
00001 /*
00002         DbView - Graph Visualization
00003     Copyright (C) 2012  Denis BEURIVE
00004 
00005     This program is free software: you can redistribute it and/or modify
00006     it under the terms of the GNU General Public License as published by
00007     the Free Software Foundation, either version 3 of the License, or
00008     (at your option) any later version.
00009 
00010     This program is distributed in the hope that it will be useful,
00011     but WITHOUT ANY WARRANTY; without even the implied warranty of
00012     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013     GNU General Public License for more details.
00014 
00015     You should have received a copy of the GNU General Public License
00016     along with this program.  If not, see <http://www.gnu.org/licenses/>.
00017 */
00018 
00019 /**
00020  * @author Denis Beurive
00021  */
00022 
00023 package org.dbview.input_addons;
00024 
00025 import java.sql.Connection;
00026 import java.sql.DriverManager;
00027 import java.sql.DatabaseMetaData;
00028 import java.sql.ResultSet;
00029 import java.util.*;
00030 import org.jdom.*;
00031 import org.dbview.db.structure.*;
00032 import org.dbview.resources.*;
00033 
00034 /**
00035  * <p>This class implements the database loader adaptor.</p>
00036  * <p>Please note that the exploration of the database relies on JDBC type 4.</p>
00037  * <p>Child classes may override the methods, if a specific database needs specific procedures.</p>
00038  * @author Denis Beurive
00039  */
00040 public abstract class AbstractLoader
00041 {
00042     /**
00043      * Handler to the database's connection.
00044      */
00045         private Connection __connection = null;
00046 
00047         /**
00048          * This attribute contains the "meta data" for the current database.
00049          * @see JDBC for details.
00050          */
00051         private DatabaseMetaData __db_meta = null;
00052 
00053         /**
00054          * This method returns an instance of a soft foreign key detector (if it is defined in the profile).
00055          * @param in_profile Profile used to open a connexion to the database through the input add-on.
00056          * @return The method returns an instance of a soft foreign key detector, or null if no detector is defined.
00057          */
00058         protected abstract AbstractSotfForeignKeyDetector _getSoftForeignKeyDetector_(Element in_profile) throws Exception;
00059 
00060         /**
00061          * This method returns the URL used to open a connection to the database.
00062          * @param in_data XML element that contains the data required to open the connection.
00063          * @return The method returns the URL used to open a connection to the database.
00064          * @throws Exception
00065          */
00066         protected abstract String _getUrl_(Element in_data) throws Exception;
00067 
00068         /**
00069      * This method returns the list of all tables in the database.
00070      * @param in_connection Connection to the database.
00071      * @param in_db_meta Meta data associated to the database.
00072      * @return The method returns the list of all tables in the database.
00073      * @throws Exception
00074      */
00075         protected ArrayList<String> _tables(Connection in_connection, DatabaseMetaData in_db_meta) throws Exception
00076         {
00077             ArrayList<String> tables  = new ArrayList<String>();
00078             ResultSet tbl = in_db_meta.getTables(in_connection.getCatalog(), "%", "%", null);
00079             while (tbl.next()) { tables.add(tbl.getString("TABLE_NAME")); }
00080             return tables;
00081         }
00082 
00083         /**
00084      * This method returns the list of all indexes for a given table.
00085      * It also set the table's list of indexes.
00086      * @param in_connection Connection to the database.
00087      * @param in_db_meta Meta data associated to the database.
00088      * @param in_table The table.
00089      * @return The method returns the list of all indexes for the given table.
00090      * @throws Exception
00091      */
00092         protected Hashtable<String, DbIndex> _indexes(Connection in_connection, DatabaseMetaData in_db_meta, Table in_table) throws Exception
00093         {
00094             String table_name = in_table.getName();
00095             Hashtable<String, DbIndex> indexes = new Hashtable<String, DbIndex>();
00096             
00097             ResultSet idxs = in_db_meta.getIndexInfo(in_connection.getCatalog(), "%", table_name, false, false);
00098         while (idxs.next())
00099         {
00100             String  field_name = idxs.getString("COLUMN_NAME");
00101             Boolean unique     = idxs.getBoolean("NON_UNIQUE") ? Boolean.FALSE : Boolean.TRUE;
00102             String  index_name = idxs.getString("INDEX_NAME");
00103             
00104             // System.out.println(">>>>> " + index_name + " " + field_name);
00105             
00106             // Is the index already listed?
00107             if (indexes.containsKey(index_name))
00108             {
00109                 // The index is already listed. We just add a new field to it.
00110                 DbIndex index = indexes.get(index_name);
00111                 index.addField(in_table.getField(field_name));
00112             }
00113             else
00114             {
00115                 // The index is not already listed.
00116                 DbIndex index = new DbIndex(table_name, unique, index_name);
00117                 index.addField(in_table.getField(field_name));
00118                 indexes.put(index_name, index);
00119             }
00120         }
00121         in_table.setIndexes(indexes);
00122             return indexes;
00123         }
00124 
00125    /**
00126     * This method returns the primary key for a given table.
00127     * @param in_connection Connection to the database.
00128     * @param in_db_meta Meta data associated to the database.
00129     * @param in_table The table.
00130     * @return <ul>
00131     *           <li>The method returns the name of the primary key, if the table defines a primary key.</li>
00132     *           <li>If the method does not define any primary key, then the method returns the value null.</li>
00133     *         </ul>
00134     * @throws Exception
00135     */
00136         protected ArrayList<String> _primary(Connection in_connection, DatabaseMetaData in_db_meta, Table in_table) throws Exception
00137         {
00138             ArrayList<String> fields = new ArrayList<String>();
00139             ResultSet pks = in_db_meta.getPrimaryKeys(in_connection.getCatalog(), "%", in_table.getName());
00140             while (pks.next()) { fields.add(pks.getString("COLUMN_NAME")); }
00141             return fields;
00142         }
00143 
00144         /**
00145      * This method returns the list of all foreign keys for a given table.
00146      * @param in_connection Connection to the database.
00147      * @param in_db_meta Meta data associated to the database.
00148      * @param in_table The table.
00149      * @return The method returns the list of all foreign keys for the given table.
00150      *         Each element of the returned list is an array that contains 2 elements.
00151      *         <ul>
00152      *              <li>First element: The name of the foreign key, within the given table.</li>
00153      *              <li>Second element: The name of the field, within the target table.</li>
00154      *         </ul>
00155      * @throws Exception
00156      */
00157         protected ArrayList<DbForeignKey> _foreign(Connection in_connection, DatabaseMetaData in_db_meta, Table in_table) throws Exception
00158         {
00159             String table_name = in_table.getName();
00160             ArrayList<DbForeignKey> foreign_keys  = new ArrayList<DbForeignKey>();
00161             ResultSet fks = in_db_meta.getImportedKeys(in_connection.getCatalog(), "%", table_name);
00162 
00163         while (fks.next())
00164         {
00165             String field_name        = fks.getString("FKCOLUMN_NAME");
00166             String target_field_name = fks.getString("PKCOLUMN_NAME");
00167             String target_table_name = fks.getString("PKTABLE_NAME");
00168             foreign_keys.add(new DbForeignKey(table_name, field_name, target_table_name, target_field_name));
00169         }
00170 
00171             return foreign_keys;
00172         }
00173 
00174         /**
00175          * This method returns the list of all fields for a given table.
00176          * @param in_connection Connection to the database.
00177          * @param in_db_meta Meta data associated to the database.
00178          * @param in_table The table.
00179          * @return The method returns the list of all fields for the given table.
00180          * @throws Exception
00181          */
00182         protected ArrayList<String> _fields(Connection in_connection, DatabaseMetaData in_db_meta, Table in_table) throws Exception
00183         {
00184             ArrayList<String> fields = new ArrayList<String>();
00185             ResultSet keys = in_db_meta.getColumns(in_connection.getCatalog(), "%", in_table.getName(), "%");
00186             while (keys.next()) { fields.add(keys.getString("COLUMN_NAME")); }
00187             return fields;
00188         }
00189 
00190         /**
00191          * This method returns the list of fields that can be null for a given table.
00192      * @param in_connection Connection to the database.
00193      * @param in_db_meta Meta data associated to the database.
00194      * @param in_table The table.
00195          * @return Thie method returns the list of fields that can be null.
00196          * @throws Exception
00197          */
00198         protected ArrayList<String> _null(Connection in_connection,DatabaseMetaData in_db_meta, Table in_table) throws Exception
00199         {
00200             ArrayList<String> fields = new ArrayList<String>();
00201             ResultSet keys = in_db_meta.getColumns(in_connection.getCatalog(), "%", in_table.getName(), "%");
00202             while (keys.next())
00203             {
00204                 if (0 == keys.getString("IS_NULLABLE").compareTo("YES"))
00205                 { fields.add(keys.getString("COLUMN_NAME")); }
00206             }
00207             return fields;
00208         }
00209 
00210         /**
00211          * This method loads information from a given database and returns a model of the database.
00212          * @param in_profile Profile associated to the database (<code>&lt;data&gt;...&lt;/data&gt;</code>).
00213          * @return The method returns a model of the database.
00214          * @throws Exception
00215          */
00216         public Database load(Element in_profile) throws Exception
00217         {
00218                 ArrayList<String> tables = null;
00219                 Database database        = null;
00220                 String URL               = null;
00221 
00222                 // Get the URL used to open the connection to the database.
00223                 URL = this._getUrl_(in_profile);
00224                 // System.out.println(URL);
00225                 
00226                 // Connect to the database.
00227                 this.__connection = DriverManager.getConnection(URL);
00228 
00229                 // Get meta data.
00230                 // The metadata includes information about the database's tables, its supported SQL grammar, its stored procedures, the capabilities of this connection, and so on. 
00231                 this.__db_meta = this.__connection.getMetaData();
00232 
00233                 // Create the database data structure.
00234                 database = new Database(this.__connection.getCatalog());
00235 
00236                 // Get the names off all tables.
00237                 tables = this._tables(this.__connection, this.__db_meta);
00238 
00239                 // For each table, get the list of primary keys and indexes.
00240                 for (String table_name: tables)
00241                 {
00242                         // Create the table, within the database.
00243                         Table table = new Table(database, table_name);
00244 
00245                         // Add all fields in the table.
00246                         for (String field_name: this._fields(this.__connection, this.__db_meta, table))
00247                         {
00248                             @SuppressWarnings("unused")
00249                             Field field = new Field(table, field_name);
00250                         }
00251 
00252                         // Set the primary keys, if exists.
00253                         ArrayList<String> primaries = this._primary(this.__connection, this.__db_meta, table);
00254                         if (null != primaries)
00255                         {
00256                             for (String primary: primaries)
00257                             { table.getField(primary).isAPrimaryKey(); }
00258                         }
00259 
00260                         // Set all fields' properties that must be set.
00261                         // Indexes set : Indexes that are composed by one and only one field.
00262                         this._indexes(this.__connection, this.__db_meta, table);
00263                         Hashtable<String, DbIndex> unique_indexes = table.getSimpleIndexes();
00264                         
00265                         for (Enumeration<String> e = unique_indexes.keys(); e.hasMoreElements(); )
00266                         {
00267                             String index_name = e.nextElement();
00268                             DbIndex index = unique_indexes.get(index_name);
00269                             String field_name = index.getField(0).getName();
00270                 if (index.unique) { table.getField(field_name).isAUniqueIndex(); }
00271                 else { table.getField(field_name).isAMultipleIndex(); }
00272                         }
00273                 }
00274                 
00275                 // For each table, get the list of hard foreign keys, and the list of null fields.
00276                 for (String table_name: tables)
00277                 {
00278                     Table table = database.getTable(table_name);
00279                     
00280                 // Set all foreign keys.
00281             for (DbForeignKey key: this._foreign(this.__connection, this.__db_meta, table))
00282             {
00283                 // Get the "reference" table.
00284                 // If the reference table is not listed, we create a dead join.
00285                 Table reference_table = database.getTable(key.reference_table_name);
00286                 
00287                 if (null == reference_table)
00288                 {
00289                     // This is a dead foreign key.
00290                     table.getField(key.field_name).isADeadforeignKey(key.reference_table_name, key.reference_field_name);
00291                     // System.out.println("Found a DFK: " + key.getFullName() + " => " +  key.reference_table_name);
00292                     continue;
00293                     // A foreign key may be in a distinct database.
00294                     // throw new Exception("Unexpected error : the database seems to be invalid. Found a foreign key (\"" + key.field_name + "\") witch reference's table (\"" + key.reference_table_name + "\") does not exist!");
00295                 }
00296                 
00297                 // Get the "reference" field.
00298                 // If the reference field is not listed, we create a dead join.
00299                 Field reference_field = reference_table.getField(key.reference_field_name);
00300                 if (null == reference_field)
00301                 { 
00302                     // This is a dead foreign key.
00303                     table.getField(key.field_name).isADeadforeignKey(key.reference_table_name, key.reference_field_name);
00304                     // System.out.println("Found a DFK: " + key.getFullName() + " => " +  key.reference_table_name);
00305                     continue;
00306                     // A foreign key may be in a distinct database.
00307                     // throw new Exception("Unexpected error : the database seems to be invalid. Found a foreign key (\"" + key.field_name + "\") witch reference's field (\"" + key.reference_table_name + "." + key.reference_field_name + "\") does not exist!");
00308                 }
00309 
00310                 // Debug reès utile:
00311                 // Field fk = table.getField(key.field_name);
00312                 // System.out.println("   (FK) " +  key.getFullName() + " -> " + target_field.getFullName());
00313                 // System.out.println("      table.getField(" + key.getFullName() + ").isAforeignKey(" + target_field.getFullName() + ")");;
00314                 // System.out.println("      Is the reference key " + target_field.getFullName() + " a primary key? " + target_field.isPrimaryKey().toString());
00315                 // System.out.println("      Is the foreign key " + fk.getFullName() + " a primary key? " + fk.isPrimaryKey().toString());
00316                 // System.out.println("      Is the foreign key " + fk.getFullName() + " (already) a foreign key? " + fk.isForeignKey().toString());
00317                 
00318                 // System.out.println(table_name + "." + key.field_name + " is a primary key");
00319                 // System.out.println("\t     > " + key.field_name);
00320                 // System.out.println("\t     > " + table.getField(key.field_name));
00321                 table.getField(key.field_name).isAforeignKey(reference_field);
00322                 table.getField(key.field_name).isAHardForeignKey();
00323             }
00324 
00325             // System.out.println("FK setted");
00326             
00327             // Set all "nullable" fields.
00328             for (String nullable_field: this._null(this.__connection, this.__db_meta, table))
00329             {
00330                 // System.out.println(nullable_field);
00331                 Field field = table.getField(nullable_field);
00332                 if (null == field) { throw new Exception("Unexpected error : the database seems to be invalid. Can not find field (\"" + table_name + "." + nullable_field + "\") ?"); }
00333                 field.isANull();
00334             }
00335                 }
00336                 
00337                 // Find for soft foreign keys.
00338                 AbstractSotfForeignKeyDetector fkDetector = this._getSoftForeignKeyDetector_(in_profile);
00339 
00340                 if (null != fkDetector)
00341                 {
00342                     for (String table_name: tables)
00343                     {
00344                         Table table = database.getTable(table_name);
00345                         for (Field field: table.getFields())
00346                         {
00347                             String field_name = field.getName();
00348                             if (! fkDetector.isFk(field_name)) { continue; }
00349 
00350                             // A hard foreign key may look like a soft one!
00351                             if (field.isForeignKey()) { continue; }
00352 
00353                             String target_table_name = fkDetector.referenceTable(field_name);
00354                             String target_field_name = fkDetector.referenceField(field_name);
00355 
00356                         // Get the target table.
00357                         Table target_table = database.getTable(target_table_name);
00358                         if (null == target_table) { throw new Exception("Unexpected error : the database seems to be invalid. Found a SOFT foreign key (\"" + field_name + "\") witch target's table (\"" + target_table_name + "\") does not exist!"); }
00359 
00360                         // Get the target field.
00361                         Field target_field = target_table.getField(target_field_name);
00362                         if (null == target_field) { throw new Exception("Unexpected error : the database seems to be invalid. Found a SOFT foreign key (\"" + field_name + "\") witch target's field (\"" + target_table_name + "." + target_field_name + "\") does not exist!"); }
00363 
00364                         // Set the field a soft foreign key.
00365                         field.isAforeignKey(target_field);
00366                         field.isASoftForeignKey();
00367                         }
00368                     }
00369                 }
00370 
00371                 // Now, generate the list of relations.
00372                 database.generateRelations();
00373 
00374 
00375                 return database;
00376         }
00377 }