source: src/main/java/weka/experiment/DatabaseUtils.java @ 15

Last change on this file since 15 was 4, checked in by gnappo, 14 years ago

Import di weka.

File size: 39.6 KB
Line 
1/*
2 *    This program is free software; you can redistribute it and/or modify
3 *    it under the terms of the GNU General Public License as published by
4 *    the Free Software Foundation; either version 2 of the License, or
5 *    (at your option) any later version.
6 *
7 *    This program is distributed in the hope that it will be useful,
8 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
9 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10 *    GNU General Public License for more details.
11 *
12 *    You should have received a copy of the GNU General Public License
13 *    along with this program; if not, write to the Free Software
14 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
15 */
16
17/*
18 *    DatabaseUtils.java
19 *    Copyright (C) 1999 University of Waikato, Hamilton, New Zealand
20 *
21 */
22
23package weka.experiment;
24
25import weka.core.RevisionHandler;
26import weka.core.RevisionUtils;
27import weka.core.Utils;
28
29import java.io.Serializable;
30import java.sql.Connection;
31import java.sql.DatabaseMetaData;
32import java.sql.DriverManager;
33import java.sql.PreparedStatement;
34import java.sql.ResultSet;
35import java.sql.ResultSetMetaData;
36import java.sql.SQLException;
37import java.sql.Statement;
38import java.sql.Types;
39import java.util.Collections;
40import java.util.HashSet;
41import java.util.Properties;
42import java.util.StringTokenizer;
43import java.util.Vector;
44
45/**
46 * DatabaseUtils provides utility functions for accessing the experiment
47 * database. The jdbc
48 * driver and database to be used default to "jdbc.idbDriver" and
49 * "jdbc:idb=experiments.prp". These may be changed by creating
50 * a java properties file called DatabaseUtils.props in user.home or
51 * the current directory. eg:<p>
52 *
53 * <code><pre>
54 * jdbcDriver=jdbc.idbDriver
55 * jdbcURL=jdbc:idb=experiments.prp
56 * </pre></code><p>
57 *
58 * @author Len Trigg (trigg@cs.waikato.ac.nz)
59 * @version $Revision: 5238 $
60 */
61public class DatabaseUtils
62  implements Serializable, RevisionHandler {
63
64  /** for serialization. */
65  static final long serialVersionUID = -8252351994547116729L;
66 
67  /** The name of the table containing the index to experiments. */
68  public static final String EXP_INDEX_TABLE = "Experiment_index";
69
70  /** The name of the column containing the experiment type (ResultProducer). */
71  public static final String EXP_TYPE_COL = "Experiment_type";
72
73  /** The name of the column containing the experiment setup (parameters). */
74  public static final String EXP_SETUP_COL = "Experiment_setup";
75 
76  /** The name of the column containing the results table name. */
77  public static final String EXP_RESULT_COL = "Result_table";
78
79  /** The prefix for result table names. */
80  public static final String EXP_RESULT_PREFIX = "Results";
81
82  /** The name of the properties file. */
83  public final static String PROPERTY_FILE = "weka/experiment/DatabaseUtils.props";
84
85  /** Holds the jdbc drivers to be used (only to stop them being gc'ed). */
86  protected Vector DRIVERS = new Vector();
87
88  /** keeping track of drivers that couldn't be loaded. */
89  protected static Vector DRIVERS_ERRORS;
90
91  /** Properties associated with the database connection. */
92  protected Properties PROPERTIES;
93
94  /* Type mapping used for reading experiment results */
95  /** Type mapping for STRING used for reading experiment results. */
96  public static final int STRING = 0;
97  /** Type mapping for BOOL used for reading experiment results. */
98  public static final int BOOL = 1;
99  /** Type mapping for DOUBLE used for reading experiment results. */
100  public static final int DOUBLE = 2;
101  /** Type mapping for BYTE used for reading experiment results. */
102  public static final int BYTE = 3;
103  /** Type mapping for SHORT used for reading experiment results. */
104  public static final int SHORT = 4;
105  /** Type mapping for INTEGER used for reading experiment results. */
106  public static final int INTEGER = 5;
107  /** Type mapping for LONG used for reading experiment results. */
108  public static final int LONG = 6;
109  /** Type mapping for FLOAT used for reading experiment results. */
110  public static final int FLOAT = 7;
111  /** Type mapping for DATE used for reading experiment results. */
112  public static final int DATE = 8; 
113  /** Type mapping for TEXT used for reading, e.g., text blobs. */
114  public static final int TEXT = 9; 
115  /** Type mapping for TIME used for reading TIME columns. */
116  public static final int TIME = 10; 
117 
118  /** Database URL. */
119  protected String m_DatabaseURL;
120 
121  /** The prepared statement used for database queries. */
122  protected transient PreparedStatement m_PreparedStatement;
123   
124  /** The database connection. */
125  protected transient Connection m_Connection;
126
127  /** True if debugging output should be printed. */
128  protected boolean m_Debug = false;
129 
130  /** Database username. */
131  protected String m_userName = "";
132
133  /** Database Password. */
134  protected String m_password = "";
135
136  /* mappings used for creating Tables. Can be overridden in DatabaseUtils.props*/
137  /** string type for the create table statement. */
138  protected String m_stringType = "LONGVARCHAR";
139  /** integer type for the create table statement. */
140  protected String m_intType = "INT";
141  /** double type for the create table statement. */
142  protected String m_doubleType = "DOUBLE";
143
144  /** For databases where Tables and Columns are created in upper case. */
145  protected boolean m_checkForUpperCaseNames = false;
146
147  /** For databases where Tables and Columns are created in lower case. */
148  protected boolean m_checkForLowerCaseNames = false;
149
150  /** setAutoCommit on the database? */
151  protected boolean m_setAutoCommit = true;
152
153  /** create index on the database? */
154  protected boolean m_createIndex = false;
155
156  /** the keywords for the current database type. */
157  protected HashSet<String> m_Keywords = new HashSet<String>();
158
159  /** the character to mask SQL keywords (by appending this character). */
160  protected String m_KeywordsMaskChar = "_";
161 
162  /**
163   * Reads properties and sets up the database drivers.
164   *
165   * @throws Exception  if an error occurs
166   */
167  public DatabaseUtils() throws Exception {
168    if (DRIVERS_ERRORS == null)
169      DRIVERS_ERRORS = new Vector();
170
171    try {
172      PROPERTIES = Utils.readProperties(PROPERTY_FILE);
173
174      // Register the drivers in jdbc DriverManager
175      String drivers = PROPERTIES.getProperty("jdbcDriver", "jdbc.idbDriver");
176
177      if (drivers == null) {
178        throw new Exception("No database drivers (JDBC) specified");
179      }
180      // The call to newInstance() is necessary on some platforms
181      // (with some java VM implementations)
182      StringTokenizer st = new StringTokenizer(drivers, ", ");
183      while (st.hasMoreTokens()) {
184        String driver = st.nextToken();
185        boolean result;
186        try {
187          Class.forName(driver);
188          DRIVERS.addElement(driver);
189          result = true;
190        }
191        catch (Exception e) {
192          result = false;
193        }
194        if (m_Debug || (!result && !DRIVERS_ERRORS.contains(driver))) 
195          System.err.println(
196              "Trying to add database driver (JDBC): " + driver
197              + " - " + (result ? "Success!" : "Error, not in CLASSPATH?"));
198        if (!result)
199          DRIVERS_ERRORS.add(driver);
200      }
201    } catch (Exception ex) {
202      System.err.println("Problem reading properties. Fix before continuing.");
203      System.err.println(ex);
204    }
205
206    m_DatabaseURL = PROPERTIES.getProperty("jdbcURL", "jdbc:idb=experiments.prp");
207    m_stringType  = PROPERTIES.getProperty("CREATE_STRING", "LONGVARCHAR");
208    m_intType     = PROPERTIES.getProperty("CREATE_INT", "INT");
209    m_doubleType  = PROPERTIES.getProperty("CREATE_DOUBLE", "DOUBLE");
210    m_checkForUpperCaseNames = PROPERTIES.getProperty(
211                                "checkUpperCaseNames", "false").equals("true");
212    m_checkForLowerCaseNames = PROPERTIES.getProperty(
213                                "checkLowerCaseNames", "false").equals("true");
214    m_setAutoCommit = PROPERTIES.getProperty(
215                        "setAutoCommit", "false").equals("true");
216    m_createIndex   = PROPERTIES.getProperty(
217                        "createIndex", "false").equals("true");
218    setKeywords(PROPERTIES.getProperty(
219                        "Keywords", "AND,ASC,BY,DESC,FROM,GROUP,INSERT,ORDER,SELECT,UPDATE,WHERE"));
220    setKeywordsMaskChar(PROPERTIES.getProperty("KeywordsMaskChar", "_"));
221  }
222 
223  /**
224   * returns key column headings in their original case. Used for
225   * those databases that create uppercase column names.
226   *
227   * @param columnName  the column to retrieve the original case for
228   * @return            the original case
229   */
230  protected String attributeCaseFix(String columnName){
231    if (m_checkForUpperCaseNames) {
232      String ucname = columnName.toUpperCase();
233      if (ucname.equals(EXP_TYPE_COL.toUpperCase())) {
234        return EXP_TYPE_COL;
235      } else if (ucname.equals(EXP_SETUP_COL.toUpperCase())) {
236        return EXP_SETUP_COL;
237      } else if (ucname.equals(EXP_RESULT_COL.toUpperCase())) {
238        return EXP_RESULT_COL;
239      } else {
240        return columnName;
241      }
242    }
243    else if (m_checkForLowerCaseNames) {
244      String ucname = columnName.toLowerCase();
245      if (ucname.equals(EXP_TYPE_COL.toLowerCase())) {
246        return EXP_TYPE_COL;
247      } else if (ucname.equals(EXP_SETUP_COL.toLowerCase())) {
248        return EXP_SETUP_COL;
249      } else if (ucname.equals(EXP_RESULT_COL.toLowerCase())) {
250        return EXP_RESULT_COL;
251      } else {
252        return columnName;
253      }
254    }
255    else {
256      return columnName;
257    }
258  }
259 
260  /**
261   * translates the column data type string to an integer value that indicates
262   * which data type / get()-Method to use in order to retrieve values from the
263   * database (see DatabaseUtils.Properties, InstanceQuery()). Blanks in the type
264   * are replaced with underscores "_", since Java property names can't contain blanks.
265   *
266   * @param type        the column type as retrieved with
267   *                    java.sql.MetaData.getColumnTypeName(int)
268   * @return            an integer value that indicates
269   *                    which data type / get()-Method to use in order to
270   *                    retrieve values from the
271   */
272  public int translateDBColumnType(String type) {
273    try {
274      // Oracle, e.g., has datatypes like "DOUBLE PRECISION"
275      // BUT property names can't have blanks in the name (unless escaped with
276      // a backslash), hence also check for names where the blanks are
277      // replaced with underscores "_":
278      String value = PROPERTIES.getProperty(type);
279      String typeUnderscore = type.replaceAll(" ", "_");
280      if (value == null)
281        value = PROPERTIES.getProperty(typeUnderscore);
282      return Integer.parseInt(value);
283    } catch (NumberFormatException e) {
284      throw new IllegalArgumentException(
285          "Unknown data type: " + type + ". "
286          + "Add entry in " + PROPERTY_FILE + ".\n"
287          + "If the type contains blanks, either escape them with a backslash "
288          + "or use underscores instead of blanks.");
289    }
290  }
291
292  /**
293   * Converts an array of objects to a string by inserting a space
294   * between each element. Null elements are printed as ?
295   *
296   * @param array       the array of objects
297   * @return            a value of type 'String'
298   */
299  public static String arrayToString(Object[] array) {
300    String result = "";
301    if (array == null) {
302      result = "<null>";
303    } else {
304      for (int i = 0; i < array.length; i++) {
305        if (array[i] == null) {
306          result += " ?";
307        } else {
308          result += " " + array[i];
309        }
310      }
311    }
312    return result;
313  }
314
315  /**
316   * Returns the name associated with a SQL type.
317   *
318   * @param type        the SQL type
319   * @return            the name of the type
320   */
321  public static String typeName(int type) {
322    switch (type) {
323      case Types.BIGINT :
324        return "BIGINT ";
325      case Types.BINARY:
326        return "BINARY";
327      case Types.BIT:
328        return "BIT";
329      case Types.CHAR:
330        return "CHAR";
331      case Types.DATE:
332        return "DATE";
333      case Types.DECIMAL:
334        return "DECIMAL";
335      case Types.DOUBLE:
336        return "DOUBLE";
337      case Types.FLOAT:
338        return "FLOAT";
339      case Types.INTEGER:
340        return "INTEGER";
341      case Types.LONGVARBINARY:
342        return "LONGVARBINARY";
343      case Types.LONGVARCHAR:
344        return "LONGVARCHAR";
345      case Types.NULL:
346        return "NULL";
347      case Types.NUMERIC:
348        return "NUMERIC";
349      case Types.OTHER:
350        return "OTHER";
351      case Types.REAL:
352        return "REAL";
353      case Types.SMALLINT:
354        return "SMALLINT";
355      case Types.TIME:
356        return "TIME";
357      case Types.TIMESTAMP:
358        return "TIMESTAMP";
359      case Types.TINYINT:
360        return "TINYINT";
361      case Types.VARBINARY:
362        return "VARBINARY";
363      case Types.VARCHAR:
364        return "VARCHAR";
365      default:
366        return "Unknown";
367    }
368  }
369
370  /**
371   * Returns the tip text for this property.
372   *
373   * @return            tip text for this property suitable for
374   *                    displaying in the explorer/experimenter gui
375   */
376  public String databaseURLTipText() {
377    return "Set the URL to the database.";
378  }
379
380  /**
381   * Get the value of DatabaseURL.
382   *
383   * @return            Value of DatabaseURL.
384   */
385  public String getDatabaseURL() {
386    return m_DatabaseURL;
387  }
388 
389  /**
390   * Set the value of DatabaseURL.
391   *
392   * @param newDatabaseURL      Value to assign to DatabaseURL.
393   */
394  public void setDatabaseURL(String newDatabaseURL) {
395    m_DatabaseURL = newDatabaseURL;
396  }
397
398  /**
399   * Returns the tip text for this property.
400   *
401   * @return            tip text for this property suitable for
402   *                    displaying in the explorer/experimenter gui
403   */
404  public String debugTipText() {
405    return "Whether debug information is printed.";
406  }
407 
408  /**
409   * Sets whether there should be printed some debugging output to stderr or not.
410   *
411   * @param d           true if output should be printed
412   */
413  public void setDebug(boolean d) {
414    m_Debug = d;
415  }
416
417  /**
418   * Gets whether there should be printed some debugging output to stderr or not.
419   *
420   * @return            true if output should be printed
421   */
422  public boolean getDebug() {
423    return m_Debug;
424  }
425
426  /**
427   * Returns the tip text for this property.
428   *
429   * @return            tip text for this property suitable for
430   *                    displaying in the explorer/experimenter gui
431   */
432  public String usernameTipText() {
433    return "The user to use for connecting to the database.";
434  }
435
436  /**
437   * Set the database username.
438   *
439   * @param username    Username for Database.
440   */
441  public void setUsername(String username){
442    m_userName = username; 
443  }
444 
445  /**
446   * Get the database username.
447   *
448   * @return            Database username
449   */
450  public String getUsername(){
451    return m_userName;
452  }
453
454  /**
455   * Returns the tip text for this property.
456   *
457   * @return            tip text for this property suitable for
458   *                    displaying in the explorer/experimenter gui
459   */
460  public String passwordTipText() {
461    return "The password to use for connecting to the database.";
462  }
463
464  /**
465   * Set the database password.
466   *
467   * @param password    Password for Database.
468   */
469  public void setPassword(String password){
470    m_password = password;
471  }
472 
473  /**
474   * Get the database password.
475   *
476   * @return            Password for Database.
477   */
478  public String getPassword(){
479    return m_password;
480  }
481
482  /**
483   * Opens a connection to the database.
484   *
485   * @throws Exception  if an error occurs
486   */
487  public void connectToDatabase() throws Exception {
488    if (m_Debug) {
489      System.err.println("Connecting to " + m_DatabaseURL);
490    }
491    if (m_Connection == null) {
492      if (m_userName.equals("")) {
493        try {
494          m_Connection = DriverManager.getConnection(m_DatabaseURL);
495        } catch (java.sql.SQLException e) {
496         
497          // Try loading the drivers
498          for (int i = 0; i < DRIVERS.size(); i++) {
499            try {
500              Class.forName((String)DRIVERS.elementAt(i));
501            } catch (Exception ex) {
502              // Drop through
503            }
504          }
505          m_Connection = DriverManager.getConnection(m_DatabaseURL);
506        }
507      } else {
508        try {
509          m_Connection = DriverManager.getConnection(m_DatabaseURL, m_userName,
510                                                     m_password);
511        } catch (java.sql.SQLException e) {
512         
513          // Try loading the drivers
514          for (int i = 0; i < DRIVERS.size(); i++) {
515            try {
516              Class.forName((String)DRIVERS.elementAt(i));
517            } catch (Exception ex) {
518              // Drop through
519            }
520          }
521          m_Connection = DriverManager.getConnection(m_DatabaseURL, m_userName,
522                                                     m_password);
523        }
524      }
525    }
526    m_Connection.setAutoCommit(m_setAutoCommit);
527  }
528
529  /**
530   * Closes the connection to the database.
531   *
532   * @throws Exception  if an error occurs
533   */
534  public void disconnectFromDatabase() throws Exception {
535    if (m_Debug) {
536      System.err.println("Disconnecting from " + m_DatabaseURL);
537    }
538    if (m_Connection != null) {
539      m_Connection.close();
540      m_Connection = null;
541    }
542  }
543 
544  /**
545   * Returns true if a database connection is active.
546   *
547   * @return            a value of type 'boolean'
548   */
549  public boolean isConnected() {
550    return (m_Connection != null);
551  }
552
553  /**
554   * Returns whether the cursors only support forward movement or are
555   * scroll sensitive (with ResultSet.CONCUR_READ_ONLY concurrency).
556   * Returns always false if not connected
557   *
558   * @return            true if connected and the cursor is scroll-sensitive
559   * @see               ResultSet#TYPE_SCROLL_SENSITIVE
560   * @see               ResultSet#TYPE_FORWARD_ONLY
561   * @see               ResultSet#CONCUR_READ_ONLY
562   */
563  public boolean isCursorScrollSensitive() {
564    boolean     result;
565   
566    result = false;
567   
568    try {
569      if (isConnected())
570        result = m_Connection.getMetaData().supportsResultSetConcurrency(
571                        ResultSet.TYPE_SCROLL_SENSITIVE, 
572                        ResultSet.CONCUR_READ_ONLY);
573    }
574    catch (Exception e) {
575      // ignored
576    }
577   
578    return result;
579  }
580 
581  /**
582   * Checks whether cursors are scrollable in general, false otherwise
583   * (also if not connected).
584   *
585   * @return            true if scrollable and connected
586   * @see               #getSupportedCursorScrollType()
587   */
588  public boolean isCursorScrollable() {
589    return (getSupportedCursorScrollType() != -1);
590  }
591 
592  /**
593   * Returns the type of scrolling that the cursor supports, -1 if not
594   * supported or not connected. Checks first for TYPE_SCROLL_SENSITIVE
595   * and then for TYPE_SCROLL_INSENSITIVE. In both cases CONCUR_READ_ONLY
596   * as concurrency is used.
597   *
598   * @return            the scroll type, or -1 if not connected or no scrolling supported
599   * @see               ResultSet#TYPE_SCROLL_SENSITIVE
600   * @see               ResultSet#TYPE_SCROLL_INSENSITIVE
601   */
602  public int getSupportedCursorScrollType() {
603    int         result;
604   
605    result = -1;
606   
607    try {
608      if (isConnected()) {
609        if (m_Connection.getMetaData().supportsResultSetConcurrency(
610                        ResultSet.TYPE_SCROLL_SENSITIVE, 
611                        ResultSet.CONCUR_READ_ONLY))
612          result = ResultSet.TYPE_SCROLL_SENSITIVE;
613       
614        if (result == -1) {
615          if (m_Connection.getMetaData().supportsResultSetConcurrency(
616                        ResultSet.TYPE_SCROLL_INSENSITIVE, 
617                        ResultSet.CONCUR_READ_ONLY))
618            result = ResultSet.TYPE_SCROLL_INSENSITIVE;
619        }
620      }
621    }
622    catch (Exception e) {
623      // ignored
624    }
625   
626    return result;
627  }
628
629  /**
630   * Executes a SQL query. Caller must clean up manually with
631   * <code>close()</code>.
632   *
633   * @param query       the SQL query
634   * @return            true if the query generated results
635   * @throws SQLException if an error occurs
636   * @see #close()
637   */
638  public boolean execute(String query) throws SQLException {
639    if (!isConnected())
640      throw new IllegalStateException("Not connected, please connect first!");
641   
642    if (!isCursorScrollable())
643      m_PreparedStatement = m_Connection.prepareStatement(
644          query, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
645    else
646      m_PreparedStatement = m_Connection.prepareStatement(
647          query, getSupportedCursorScrollType(), ResultSet.CONCUR_READ_ONLY);
648   
649    return(m_PreparedStatement.execute());
650  }
651
652  /**
653   * Gets the results generated by a previous query. Caller must clean up
654   * manually with <code>close(ResultSet)</code>. Returns null if object has
655   * been deserialized.
656   *
657   * @return            the result set.
658   * @throws SQLException if an error occurs
659   * @see #close(ResultSet)
660   */
661  public ResultSet getResultSet() throws SQLException {
662    if (m_PreparedStatement != null)
663      return m_PreparedStatement.getResultSet();
664    else
665      return null;
666  }
667
668  /**
669   * Executes a SQL DDL query or an INSERT, DELETE or UPDATE.
670   *
671   * @param query       the SQL DDL query
672   * @return            the number of affected rows
673   * @throws SQLException if an error occurs
674   */
675  public int update(String query) throws SQLException {
676    if (!isConnected())
677      throw new IllegalStateException("Not connected, please connect first!");
678   
679    Statement statement;
680    if (!isCursorScrollable())
681      statement = m_Connection.createStatement(
682          ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
683    else
684      statement = m_Connection.createStatement(
685          getSupportedCursorScrollType(), ResultSet.CONCUR_READ_ONLY);
686    int result = statement.executeUpdate(query);
687    statement.close();
688   
689    return result;
690  }
691
692  /**
693   * Executes a SQL SELECT query that returns a ResultSet. Note: the ResultSet
694   * object must be closed by the caller.
695   *
696   * @param query       the SQL query
697   * @return            the generated ResultSet
698   * @throws SQLException if an error occurs
699   */
700  public ResultSet select(String query) throws SQLException {
701    if (!isConnected())
702      throw new IllegalStateException("Not connected, please connect first!");
703   
704    Statement statement;
705    if (!isCursorScrollable())
706      statement = m_Connection.createStatement(
707          ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
708    else
709      statement = m_Connection.createStatement(
710          getSupportedCursorScrollType(), ResultSet.CONCUR_READ_ONLY);
711    ResultSet result = statement.executeQuery(query);
712   
713    return result;
714  }
715
716  /**
717   * closes the ResultSet and the statement that generated the ResultSet to
718   * avoid memory leaks in JDBC drivers - in contrast to the JDBC specs, a lot
719   * of JDBC drives don't clean up correctly.
720   *
721   * @param rs          the ResultSet to clean up
722   */
723  public void close(ResultSet rs) {
724    try {
725      Statement statement = rs.getStatement();
726      rs.close();
727      statement.close();
728      statement = null;
729      rs = null;
730    }
731    catch (Exception e) {
732      // ignored
733    }
734  }
735 
736  /**
737   * closes the m_PreparedStatement to avoid memory leaks.
738   */
739  public void close() {
740    if (m_PreparedStatement != null) {
741      try {
742        m_PreparedStatement.close();
743        m_PreparedStatement = null;
744      }
745      catch (Exception e) {
746        // ignored
747      }
748    }
749  }
750 
751  /**
752   * Checks that a given table exists.
753   *
754   * @param tableName   the name of the table to look for.
755   * @return            true if the table exists.
756   * @throws Exception  if an error occurs.
757   */
758  public boolean tableExists(String tableName) throws Exception {
759    if (!isConnected())
760      throw new IllegalStateException("Not connected, please connect first!");
761   
762    if (m_Debug) {
763      System.err.println("Checking if table " + tableName + " exists...");
764    }
765    DatabaseMetaData dbmd = m_Connection.getMetaData();
766    ResultSet rs;
767    if (m_checkForUpperCaseNames) {
768      rs = dbmd.getTables (null, null, tableName.toUpperCase(), null);
769    } else if (m_checkForLowerCaseNames) {
770      rs = dbmd.getTables (null, null, tableName.toLowerCase(), null);
771    } else {
772      rs = dbmd.getTables (null, null, tableName, null);
773    }
774    boolean tableExists = rs.next();
775    if (rs.next()) {
776      throw new Exception("This table seems to exist more than once!");
777    }
778    rs.close();
779    if (m_Debug) {
780      if (tableExists) {
781        System.err.println("... " + tableName + " exists");
782      } else {
783        System.err.println("... " + tableName + " does not exist");
784      }
785    }
786    return tableExists;
787  }
788
789  /**
790   * processes the string in such a way that it can be stored in the
791   * database, i.e., it changes backslashes into slashes and doubles single
792   * quotes.
793   *
794   * @param s           the string to work on
795   * @return            the processed string
796   */
797  public static String processKeyString(String s) {
798    return s.replaceAll("\\\\", "/").replaceAll("'", "''");
799  }
800 
801  /**
802   * Executes a database query to see whether a result for the supplied key
803   * is already in the database.           
804   *
805   * @param tableName   the name of the table to search for the key in
806   * @param rp          the ResultProducer who will generate the result if
807   *                    required
808   * @param key         the key for the result
809   * @return            true if the result with that key is in the database
810   *                    already
811   * @throws Exception  if an error occurs
812   */
813  protected boolean isKeyInTable(String tableName,
814                                 ResultProducer rp,
815                                 Object[] key)
816    throws Exception {
817
818    String query = "SELECT Key_Run"
819      + " FROM " + tableName;
820    String [] keyNames = rp.getKeyNames();
821    if (keyNames.length != key.length) {
822      throw new Exception("Key names and key values of different lengths");
823    }
824    boolean first = true;
825    for (int i = 0; i < key.length; i++) {
826      if (key[i] != null) {
827        if (first) {
828          query += " WHERE ";
829          first = false;
830        } else {
831          query += " AND ";
832        }
833        query += "Key_" + keyNames[i] + '=';
834        if (key[i] instanceof String) {
835          query += "'" + processKeyString(key[i].toString()) + "'";
836        } else {
837          query += key[i].toString();
838        }
839      }
840    }
841    boolean retval = false;
842    ResultSet rs = select(query);
843    if (rs.next()) {
844      retval = true;
845      if (rs.next()) {
846        throw new Exception("More than one result entry "
847            + "for result key: " + query);
848      }
849    }
850    close(rs);
851    return retval;
852  }
853
854  /**
855   * Executes a database query to extract a result for the supplied key
856   * from the database.           
857   *
858   * @param tableName   the name of the table where the result is stored
859   * @param rp          the ResultProducer who will generate the result if
860   *                    required
861   * @param key         the key for the result
862   * @return            true if the result with that key is in the database
863   *                    already
864   * @throws Exception  if an error occurs
865   */
866  public Object[] getResultFromTable(String tableName,
867                                         ResultProducer rp,
868                                         Object [] key)
869    throws Exception {
870
871    String query = "SELECT ";
872    String [] resultNames = rp.getResultNames();
873    for (int i = 0; i < resultNames.length; i++) {
874      if (i != 0) {
875        query += ", ";
876      }
877      query += resultNames[i];
878    }
879    query += " FROM " + tableName;
880    String [] keyNames = rp.getKeyNames();
881    if (keyNames.length != key.length) {
882      throw new Exception("Key names and key values of different lengths");
883    }
884    boolean first = true;
885    for (int i = 0; i < key.length; i++) {
886      if (key[i] != null) {
887        if (first) {
888          query += " WHERE ";
889          first = false;
890        } else {
891          query += " AND ";
892        }
893        query += "Key_" + keyNames[i] + '=';
894        if (key[i] instanceof String) {
895          query += "'" + processKeyString(key[i].toString()) + "'";
896        } else {
897          query += key[i].toString();
898        }
899      }
900    }
901    ResultSet rs = select(query);
902    ResultSetMetaData md = rs.getMetaData();
903    int numAttributes = md.getColumnCount();
904    if (!rs.next()) {
905      throw new Exception("No result for query: " + query);
906    }
907    // Extract the columns for the result
908    Object [] result = new Object [numAttributes];
909    for(int i = 1; i <= numAttributes; i++) {
910      switch (translateDBColumnType(md.getColumnTypeName(i))) {
911        case STRING : 
912          result[i - 1] = rs.getString(i);
913          if (rs.wasNull()) {
914            result[i - 1] = null;
915          }
916          break;
917        case FLOAT:
918        case DOUBLE:
919          result[i - 1] = new Double(rs.getDouble(i));
920          if (rs.wasNull()) {
921            result[i - 1] = null;
922          }
923          break;
924        default:
925          throw new Exception("Unhandled SQL result type (field " + (i + 1)
926              + "): "
927              + DatabaseUtils.typeName(md.getColumnType(i)));
928      }
929    }
930    if (rs.next()) {
931      throw new Exception("More than one result entry "
932                          + "for result key: " + query);
933    }
934    close(rs);
935    return result;
936  }
937
938  /**
939   * Executes a database query to insert a result for the supplied key
940   * into the database.           
941   *
942   * @param tableName   the name of the table where the result is stored
943   * @param rp          the ResultProducer who will generate the result if
944   *                    required
945   * @param key         the key for the result
946   * @param result      the result to store
947   * @throws Exception  if an error occurs
948   */
949  public void putResultInTable(String tableName,
950                               ResultProducer rp,
951                               Object [] key,
952                               Object [] result)
953    throws Exception {
954   
955    String query = "INSERT INTO " + tableName
956      + " VALUES ( ";
957    // Add the results to the table
958    for (int i = 0; i < key.length; i++) {
959      if (i != 0) {
960        query += ',';
961      }
962      if (key[i] != null) {
963        if (key[i] instanceof String) {
964          query += "'" + processKeyString(key[i].toString()) + "'";
965        } else if (key[i] instanceof Double) {
966          query += safeDoubleToString((Double)key[i]);
967        } else {
968          query += key[i].toString();
969        }
970      } else {
971        query += "NULL";
972      }
973    }
974    for (int i = 0; i < result.length; i++) {
975      query +=  ',';
976      if (result[i] != null) {
977        if (result[i] instanceof String) {
978          query += "'" + result[i].toString() + "'";
979        } else  if (result[i] instanceof Double) {
980          query += safeDoubleToString((Double)result[i]);
981        } else {
982          query += result[i].toString();
983          //!!
984          //System.err.println("res: "+ result[i].toString());
985        }
986      } else {
987        query += "NULL";
988      }
989    }
990    query += ')';
991   
992    if (m_Debug) {
993      System.err.println("Submitting result: " + query);
994    }
995    update(query);
996    close();
997  }
998 
999  /**
1000   * Inserts a + if the double is in scientific notation.
1001   * MySQL doesn't understand the number otherwise.
1002   *
1003   * @param number      the number to convert
1004   * @return            the number as string
1005   */
1006  private String safeDoubleToString(Double number) {
1007    // NaN is treated as NULL
1008    if (number.isNaN())
1009      return "NULL";
1010
1011    String orig = number.toString();
1012
1013    int pos = orig.indexOf('E');
1014    if ((pos == -1) || (orig.charAt(pos + 1) == '-')) {
1015      return orig;
1016    } else {
1017      StringBuffer buff = new StringBuffer(orig);
1018      buff.insert(pos + 1, '+');
1019      return new String(buff);
1020    }
1021  }
1022
1023  /**
1024   * Returns true if the experiment index exists.
1025   *
1026   * @return            true if the index exists
1027   * @throws Exception  if an error occurs
1028   */
1029  public boolean experimentIndexExists() throws Exception {
1030    return tableExists(EXP_INDEX_TABLE);
1031  }
1032 
1033  /**
1034   * Attempts to create the experiment index table.
1035   *
1036   * @throws Exception  if an error occurs.
1037   */
1038  public void createExperimentIndex() throws Exception {
1039    if (m_Debug) {
1040      System.err.println("Creating experiment index table...");
1041    }
1042    String query;
1043
1044    // Workaround for MySQL (doesn't support LONGVARBINARY)
1045    // Also for InstantDB which attempts to interpret numbers when storing
1046    // in LONGVARBINARY
1047    /* if (m_Connection.getMetaData().getDriverName().
1048        equals("Mark Matthews' MySQL Driver")
1049        || (m_Connection.getMetaData().getDriverName().
1050        indexOf("InstantDB JDBC Driver") != -1)) {
1051      query = "CREATE TABLE " + EXP_INDEX_TABLE
1052        + " ( " + EXP_TYPE_COL + " TEXT,"
1053        + "  " + EXP_SETUP_COL + " TEXT,"
1054        + "  " + EXP_RESULT_COL + " INT )";
1055        } else { */
1056   
1057      query = "CREATE TABLE " + EXP_INDEX_TABLE
1058        + " ( " + EXP_TYPE_COL + " "+ m_stringType+","
1059        + "  " + EXP_SETUP_COL + " "+ m_stringType+","
1060        + "  " + EXP_RESULT_COL + " "+ m_intType+" )";
1061      // }
1062    // Other possible fields:
1063    //   creator user name (from System properties)
1064    //   creation date
1065    update(query);
1066    close();
1067  }
1068
1069  /**
1070   * Attempts to insert a results entry for the table into the
1071   * experiment index.
1072   *
1073   * @param rp          the ResultProducer generating the results
1074   * @return            the name of the created results table
1075   * @throws Exception  if an error occurs.
1076   */
1077  public String createExperimentIndexEntry(ResultProducer rp)
1078    throws Exception {
1079
1080    if (m_Debug) {
1081      System.err.println("Creating experiment index entry...");
1082    }
1083
1084    // Execute compound transaction
1085    int numRows = 0;
1086   
1087    // Workaround for MySQL (doesn't support transactions)
1088    /*  if (m_Connection.getMetaData().getDriverName().
1089        equals("Mark Matthews' MySQL Driver")) {
1090      m_Statement.execute("LOCK TABLES " + EXP_INDEX_TABLE + " WRITE");
1091      System.err.println("LOCKING TABLE");
1092      } else {*/
1093     
1094      //}
1095
1096    // Get the number of rows
1097    String query = "SELECT COUNT(*) FROM " + EXP_INDEX_TABLE;
1098    ResultSet rs = select(query);
1099    if (m_Debug) {
1100      System.err.println("...getting number of rows");
1101    }
1102    if (rs.next()) {
1103      numRows = rs.getInt(1);
1104    }
1105    close(rs);
1106
1107    // Add an entry in the index table
1108    String expType = rp.getClass().getName();
1109    String expParams = rp.getCompatibilityState();
1110    query = "INSERT INTO " + EXP_INDEX_TABLE
1111      +" VALUES ('"
1112      + expType + "', '" + expParams
1113      + "', " + numRows + " )"; 
1114    if (update(query) > 0) {
1115      if (m_Debug) {
1116        System.err.println("...create returned resultset");
1117      }
1118    }
1119    close();
1120   
1121    // Finished compound transaction
1122    // Workaround for MySQL (doesn't support transactions)
1123    /* if (m_Connection.getMetaData().getDriverName().
1124        equals("Mark Matthews' MySQL Driver")) {
1125      m_Statement.execute("UNLOCK TABLES");
1126      System.err.println("UNLOCKING TABLE");
1127      } else { */
1128    if (!m_setAutoCommit) {
1129      m_Connection.commit();
1130      m_Connection.setAutoCommit(true);
1131    }
1132      //}
1133
1134    String tableName = getResultsTableName(rp);
1135    if (tableName == null) {
1136      throw new Exception("Problem adding experiment index entry");
1137    }
1138
1139    // Drop any existing table by that name (shouldn't occur unless
1140    // the experiment index is destroyed, in which case the experimental
1141    // conditions of the existing table are unknown)
1142    try {
1143      query = "DROP TABLE " + tableName;
1144      if (m_Debug) {
1145        System.err.println(query);
1146      }
1147      update(query);
1148    } catch (SQLException ex) {
1149      System.err.println(ex.getMessage());
1150    }
1151    return tableName;
1152  }
1153
1154  /**
1155   * Gets the name of the experiment table that stores results from a
1156   * particular ResultProducer.
1157   *
1158   * @param rp          the ResultProducer
1159   * @return            the name of the table where the results for this
1160   *                    ResultProducer are stored, or null if there is no
1161   *                    table for this ResultProducer.
1162   * @throws Exception  if an error occurs
1163   */
1164  public String getResultsTableName(ResultProducer rp) throws Exception {
1165    // Get the experiment table name, or create a new table if necessary.
1166    if (m_Debug) {
1167      System.err.println("Getting results table name...");
1168    }
1169    String expType = rp.getClass().getName();
1170    String expParams = rp.getCompatibilityState();
1171    String query = "SELECT " + EXP_RESULT_COL
1172      + " FROM " + EXP_INDEX_TABLE
1173       + " WHERE " + EXP_TYPE_COL + "='" + expType
1174      + "' AND " + EXP_SETUP_COL + "='" + expParams + "'";
1175    String tableName = null;
1176    ResultSet rs = select(query);
1177    if (rs.next()) {
1178      tableName = rs.getString(1);
1179      if (rs.next()) {
1180        throw new Exception("More than one index entry "
1181            + "for experiment config: " + query);
1182      }
1183    }
1184    close(rs);
1185    if (m_Debug) {
1186      System.err.println("...results table = " + ((tableName == null) 
1187                                                  ? "<null>" 
1188                                                  : EXP_RESULT_PREFIX
1189                                                  + tableName));
1190    }
1191    return (tableName == null) ? tableName : EXP_RESULT_PREFIX + tableName;
1192  }
1193
1194  /**
1195   * Creates a results table for the supplied result producer.
1196   *
1197   * @param rp          the ResultProducer generating the results
1198   * @param tableName   the name of the resultsTable
1199   * @return            the name of the created results table
1200   * @throws Exception  if an error occurs.
1201   */
1202  public String createResultsTable(ResultProducer rp, String tableName)
1203    throws Exception {
1204
1205    if (m_Debug) {
1206      System.err.println("Creating results table " + tableName + "...");
1207    }
1208    String query = "CREATE TABLE " + tableName + " ( ";
1209    // Loop over the key fields
1210    String [] names = rp.getKeyNames();
1211    Object [] types = rp.getKeyTypes();
1212    if (names.length != types.length) {
1213      throw new Exception("key names types differ in length");
1214    }
1215    for (int i = 0; i < names.length; i++) {
1216      query += "Key_" + names[i] + " ";
1217      if (types[i] instanceof Double) {
1218        query += m_doubleType;
1219      } else if (types[i] instanceof String) {
1220
1221        // Workaround for MySQL (doesn't support LONGVARCHAR)
1222        // Also for InstantDB which attempts to interpret numbers when storing
1223        // in LONGVARBINARY
1224        /*if (m_Connection.getMetaData().getDriverName().
1225            equals("Mark Matthews' MySQL Driver")
1226            || (m_Connection.getMetaData().getDriverName().
1227                indexOf("InstantDB JDBC Driver")) != -1) {
1228          query += "TEXT ";
1229          } else { */
1230        //query += "LONGVARCHAR ";
1231          query += m_stringType+" ";
1232          //}
1233      } else {
1234        throw new Exception("Unknown/unsupported field type in key");
1235      }
1236      query += ", ";
1237    }
1238    // Loop over the result fields
1239    names = rp.getResultNames();
1240    types = rp.getResultTypes();
1241    if (names.length != types.length) {
1242      throw new Exception("result names and types differ in length");
1243    }
1244    for (int i = 0; i < names.length; i++) {
1245      query += names[i] + " ";
1246      if (types[i] instanceof Double) {
1247        query += m_doubleType;
1248      } else if (types[i] instanceof String) {
1249       
1250        // Workaround for MySQL (doesn't support LONGVARCHAR)
1251        // Also for InstantDB which attempts to interpret numbers when storing
1252        // in LONGVARBINARY
1253        /*if (m_Connection.getMetaData().getDriverName().
1254            equals("Mark Matthews' MySQL Driver")
1255            || (m_Connection.getMetaData().getDriverName().
1256                equals("InstantDB JDBC Driver"))) {
1257          query += "TEXT ";
1258          } else {*/
1259        //query += "LONGVARCHAR ";
1260        query += m_stringType+" ";
1261          //}
1262      } else {
1263        throw new Exception("Unknown/unsupported field type in key");
1264      }
1265      if (i < names.length - 1) {
1266        query += ", ";
1267      }
1268    }
1269    query += " )";
1270   
1271    update(query);
1272    if (m_Debug) 
1273      System.err.println("table created");
1274    close();
1275
1276
1277    if (m_createIndex) {
1278      query = "CREATE UNIQUE INDEX Key_IDX ON "+ tableName +" (";
1279
1280      String [] keyNames = rp.getKeyNames();
1281   
1282      boolean first = true;
1283      for (int i = 0; i < keyNames.length; i++) {
1284        if (keyNames[i] != null) {
1285          if (first) {
1286            first = false;
1287            query += "Key_" + keyNames[i];
1288          } else {
1289            query += ",Key_" + keyNames[i];
1290          } 
1291        }
1292      }
1293      query += ")";
1294   
1295      update(query);
1296    }
1297    return tableName;
1298  }
1299 
1300  /**
1301   * Sets the keywords (comma-separated list) to use.
1302   *
1303   * @param value       the list of keywords
1304   */
1305  public void setKeywords(String value) {
1306    String[]    keywords;
1307    int         i;
1308   
1309    m_Keywords.clear();
1310   
1311    keywords = value.replaceAll(" ", "").split(",");
1312    for (i = 0; i < keywords.length; i++)
1313      m_Keywords.add(keywords[i].toUpperCase());
1314  }
1315 
1316  /**
1317   * Returns the currently stored keywords (as comma-separated list).
1318   *
1319   * @return            the list of keywords
1320   */
1321  public String getKeywords() {
1322    String              result;
1323    Vector<String>      list;
1324    int                 i;
1325   
1326    list = new Vector<String>(m_Keywords);
1327    Collections.sort(list);
1328   
1329    result = "";
1330    for (i = 0; i < list.size(); i++) {
1331      if (i > 0)
1332        result += ",";
1333      result += list.get(i);
1334    }
1335   
1336    return result;
1337  }
1338 
1339  /**
1340   * Sets the mask character to append to table or attribute names that
1341   * are a reserved keyword.
1342   *
1343   * @param value       the new character
1344   */
1345  public void setKeywordsMaskChar(String value) {
1346    m_KeywordsMaskChar = value;
1347  }
1348 
1349  /**
1350   * Returns the currently set mask character.
1351   *
1352   * @return            the character
1353   */
1354  public String getKeywordsMaskChar() {
1355    return m_KeywordsMaskChar;
1356  }
1357 
1358  /**
1359   * Checks whether the given string is a reserved keyword.
1360   *
1361   * @param s           the string to check
1362   * @return            true if the string is a keyword
1363   * @see               #m_Keywords
1364   */
1365  public boolean isKeyword(String s) {
1366    return m_Keywords.contains(s.toUpperCase());
1367  }
1368 
1369  /**
1370   * If the given string is a keyword, then the mask character will be
1371   * appended and returned. Otherwise, the same string will be returned
1372   * unchanged.
1373   *
1374   * @param s           the string to check
1375   * @return            the potentially masked string
1376   * @see               #m_KeywordsMaskChar
1377   * @see               #isKeyword(String)
1378   */
1379  public String maskKeyword(String s) {
1380    if (isKeyword(s))
1381      return s + m_KeywordsMaskChar;
1382    else
1383      return s;
1384  }
1385 
1386  /**
1387   * Returns the revision string.
1388   *
1389   * @return            the revision
1390   */
1391  public String getRevision() {
1392    return RevisionUtils.extract("$Revision: 5238 $");
1393  }
1394}
Note: See TracBrowser for help on using the repository browser.