Source for org.jfree.data.jdbc.JDBCXYDataset

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2006, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * ------------------
  28:  * JDBCXYDataset.java
  29:  * ------------------
  30:  * (C) Copyright 2002-2006, by Bryan Scott and Contributors.
  31:  *
  32:  * Original Author:  Bryan Scott;
  33:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  34:  *                   Eric Alexander;
  35:  *
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 14-Mar-2002 : Version 1 contributed by Bryan Scott (DG);
  40:  * 19-Apr-2002 : Updated executeQuery, to close cursors and to improve support 
  41:  *               for types.
  42:  * 26-Apr-2002 : Renamed JdbcXYDataset to better fit in with the existing data 
  43:  *               source conventions.
  44:  * 26-Apr-2002 : Changed to extend AbstractDataset.
  45:  * 13-Aug-2002 : Updated Javadoc comments and imports (DG);
  46:  * 18-Sep-2002 : Updated to support BIGINT (BS);
  47:  * 21-Jan-2003 : Renamed JdbcXYDataset --> JDBCXYDataset (DG);
  48:  * 01-Jul-2003 : Added support to query whether a timeseries (BS);
  49:  * 30-Jul-2003 : Added empty contructor and executeQuery(connection,string) 
  50:  *               method (BS);
  51:  * 24-Sep-2003 : Added a check to ensure at least two valid columns are 
  52:  *               returned by the query in executeQuery as suggest in online 
  53:  *               forum by anonymous (BS);
  54:  * 02-Dec-2003 : Throwing exceptions allows to handle errors, removed default 
  55:  *               constructor, as without a connection, a query can never be 
  56:  *               executed.
  57:  * 16-Mar-2004 : Added check for null values (EA);
  58:  * 05-May-2004 : Now extends AbstractXYDataset (DG);
  59:  * 21-May-2004 : Implemented TableXYDataset, added support for SMALLINT and 
  60:  *               fixed bug in code that determines the min and max values (see 
  61:  *               bug id 938138) (DG);
  62:  * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
  63:  *               getYValue() (DG);
  64:  * 18-Nov-2004 : Updated for changes in RangeInfo interface (DG);
  65:  * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 
  66:  *               release (DG);
  67:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  68:  * 17-Oct-2006 : Deprecated unused methods - see bug 1578293 (DG);
  69:  * 
  70:  */
  71: 
  72: package org.jfree.data.jdbc;
  73: 
  74: import java.sql.Connection;
  75: import java.sql.DriverManager;
  76: import java.sql.ResultSet;
  77: import java.sql.ResultSetMetaData;
  78: import java.sql.SQLException;
  79: import java.sql.Statement;
  80: import java.sql.Types;
  81: import java.util.ArrayList;
  82: import java.util.Date;
  83: 
  84: import org.jfree.data.Range;
  85: import org.jfree.data.RangeInfo;
  86: import org.jfree.data.general.Dataset;
  87: import org.jfree.data.xy.AbstractXYDataset;
  88: import org.jfree.data.xy.TableXYDataset;
  89: import org.jfree.data.xy.XYDataset;
  90: import org.jfree.util.Log;
  91: 
  92: /**
  93:  * This class provides an {@link XYDataset} implementation over a database 
  94:  * JDBC result set.  The dataset is populated via a call to executeQuery with 
  95:  * the string sql query.  The sql query must return at least two columns.  
  96:  * The first column will be the x-axis and remaining columns y-axis values.
  97:  * executeQuery can be called a number of times.
  98:  *
  99:  * The database connection is read-only and no write back facility exists.
 100:  */
 101: public class JDBCXYDataset extends AbstractXYDataset 
 102:                            implements XYDataset, 
 103:                                       TableXYDataset, 
 104:                                       RangeInfo {
 105: 
 106:     /** The database connection. */
 107:     private transient Connection connection;
 108: 
 109:     /** Column names. */
 110:     private String[] columnNames = {};
 111: 
 112:     /** Rows. */
 113:     private ArrayList rows;
 114: 
 115:     /** The maximum y value of the returned result set */
 116:     private double maxValue = 0.0;
 117: 
 118:     /** The minimum y value of the returned result set */
 119:     private double minValue = 0.0;
 120: 
 121:     /** Is this dataset a timeseries ? */
 122:     private boolean isTimeSeries = false;
 123: 
 124:     /**
 125:      * Creates a new JDBCXYDataset (initially empty) with no database 
 126:      * connection.
 127:      */
 128:     private JDBCXYDataset() {
 129:         this.rows = new ArrayList();
 130:     }
 131: 
 132:     /**
 133:      * Creates a new dataset (initially empty) and establishes a new database 
 134:      * connection.
 135:      *
 136:      * @param  url  URL of the database connection.
 137:      * @param  driverName  the database driver class name.
 138:      * @param  user  the database user.
 139:      * @param  password  the database user's password.
 140:      * 
 141:      * @throws ClassNotFoundException if the driver cannot be found.
 142:      * @throws SQLException if there is a problem connecting to the database.
 143:      */
 144:     public JDBCXYDataset(String url,
 145:                          String driverName,
 146:                          String user,
 147:                          String password)
 148:         throws SQLException, ClassNotFoundException {
 149:         
 150:         this();
 151:         Class.forName(driverName);
 152:         this.connection = DriverManager.getConnection(url, user, password);
 153:     }
 154: 
 155:     /**
 156:      * Creates a new dataset (initially empty) using the specified database 
 157:      * connection.
 158:      *
 159:      * @param  con  the database connection.
 160:      * 
 161:      * @throws SQLException if there is a problem connecting to the database.
 162:      */
 163:     public JDBCXYDataset(Connection con) throws SQLException {
 164:         this();
 165:         this.connection = con;
 166:     }
 167: 
 168:     /**
 169:      * Creates a new dataset using the specified database connection, and 
 170:      * populates it using data obtained with the supplied query.
 171:      *
 172:      * @param con  the connection.
 173:      * @param query  the SQL query.
 174:      * 
 175:      * @throws SQLException if there is a problem executing the query.
 176:      */
 177:     public JDBCXYDataset(Connection con, String query) throws SQLException {
 178:         this(con);
 179:         executeQuery(query);
 180:     }
 181: 
 182:     /**
 183:      * Returns <code>true</code> if the dataset represents time series data, 
 184:      * and <code>false</code> otherwise.
 185:      * 
 186:      * @return A boolean.
 187:      */
 188:     public boolean isTimeSeries() {
 189:         return this.isTimeSeries;
 190:     }
 191: 
 192:     /**
 193:      * Sets a flag that indicates whether or not the data represents a time 
 194:      * series.
 195:      * 
 196:      * @param timeSeries  the new value of the flag.
 197:      */
 198:     public void setTimeSeries(boolean timeSeries) {
 199:         this.isTimeSeries = timeSeries;
 200:     }
 201: 
 202:     /**
 203:      * ExecuteQuery will attempt execute the query passed to it against the
 204:      * existing database connection.  If no connection exists then no action
 205:      * is taken.
 206:      *
 207:      * The results from the query are extracted and cached locally, thus
 208:      * applying an upper limit on how many rows can be retrieved successfully.
 209:      *
 210:      * @param  query  the query to be executed.
 211:      * 
 212:      * @throws SQLException if there is a problem executing the query.
 213:      */
 214:     public void executeQuery(String query) throws SQLException {
 215:         executeQuery(this.connection, query);
 216:     }
 217: 
 218:     /**
 219:      * ExecuteQuery will attempt execute the query passed to it against the
 220:      * provided database connection.  If connection is null then no action is 
 221:      * taken.
 222:      *
 223:      * The results from the query are extracted and cached locally, thus
 224:      * applying an upper limit on how many rows can be retrieved successfully.
 225:      *
 226:      * @param  query  the query to be executed.
 227:      * @param  con  the connection the query is to be executed against.
 228:      * 
 229:      * @throws SQLException if there is a problem executing the query.
 230:      */
 231:     public void executeQuery(Connection con, String query) 
 232:         throws SQLException {
 233: 
 234:         if (con == null) {
 235:             throw new SQLException(
 236:                 "There is no database to execute the query."
 237:             );
 238:         }
 239: 
 240:         ResultSet resultSet = null;
 241:         Statement statement = null;
 242:         try {
 243:             statement = con.createStatement();
 244:             resultSet = statement.executeQuery(query);
 245:             ResultSetMetaData metaData = resultSet.getMetaData();
 246: 
 247:             int numberOfColumns = metaData.getColumnCount();
 248:             int numberOfValidColumns = 0;
 249:             int [] columnTypes = new int[numberOfColumns];
 250:             for (int column = 0; column < numberOfColumns; column++) {
 251:                 try {
 252:                     int type = metaData.getColumnType(column + 1);
 253:                     switch (type) {
 254: 
 255:                         case Types.NUMERIC:
 256:                         case Types.REAL:
 257:                         case Types.INTEGER:
 258:                         case Types.DOUBLE:
 259:                         case Types.FLOAT:
 260:                         case Types.DECIMAL:
 261:                         case Types.BIT:
 262:                         case Types.DATE:
 263:                         case Types.TIME:
 264:                         case Types.TIMESTAMP:
 265:                         case Types.BIGINT:
 266:                         case Types.SMALLINT:
 267:                             ++numberOfValidColumns;
 268:                             columnTypes[column] = type;
 269:                             break;
 270:                         default:
 271:                             Log.warn(
 272:                                 "Unable to load column "
 273:                                 + column + " (" + type + ","
 274:                                 + metaData.getColumnClassName(column + 1) 
 275:                                 + ")"
 276:                             );
 277:                             columnTypes[column] = Types.NULL;
 278:                             break;
 279:                     }
 280:                 }
 281:                 catch (SQLException e) {
 282:                     columnTypes[column] = Types.NULL;
 283:                     throw e;
 284:                 }
 285:             }
 286: 
 287: 
 288:             if (numberOfValidColumns <= 1) {
 289:                 throw new SQLException(
 290:                     "Not enough valid columns where generated by query."
 291:                 );
 292:             }
 293: 
 294:             /// First column is X data
 295:             this.columnNames = new String[numberOfValidColumns - 1];
 296:             /// Get the column names and cache them.
 297:             int currentColumn = 0;
 298:             for (int column = 1; column < numberOfColumns; column++) {
 299:                 if (columnTypes[column] != Types.NULL) {
 300:                     this.columnNames[currentColumn] 
 301:                         = metaData.getColumnLabel(column + 1);
 302:                     ++currentColumn;
 303:                 }
 304:             }
 305: 
 306:             // Might need to add, to free memory from any previous result sets
 307:             if (this.rows != null) {
 308:                 for (int column = 0; column < this.rows.size(); column++) {
 309:                     ArrayList row = (ArrayList) this.rows.get(column);
 310:                     row.clear();
 311:                 }
 312:                 this.rows.clear();
 313:             }
 314: 
 315:             // Are we working with a time series.
 316:             switch (columnTypes[0]) {
 317:                 case Types.DATE:
 318:                 case Types.TIME:
 319:                 case Types.TIMESTAMP:
 320:                     this.isTimeSeries = true;
 321:                     break;
 322:                 default :
 323:                     this.isTimeSeries = false;
 324:                     break;
 325:             }
 326: 
 327:             // Get all rows.
 328:             // rows = new ArrayList();
 329:             while (resultSet.next()) {
 330:                 ArrayList newRow = new ArrayList();
 331:                 for (int column = 0; column < numberOfColumns; column++) {
 332:                     Object xObject = resultSet.getObject(column + 1);
 333:                     switch (columnTypes[column]) {
 334:                         case Types.NUMERIC:
 335:                         case Types.REAL:
 336:                         case Types.INTEGER:
 337:                         case Types.DOUBLE:
 338:                         case Types.FLOAT:
 339:                         case Types.DECIMAL:
 340:                         case Types.BIGINT:
 341:                         case Types.SMALLINT:
 342:                             newRow.add(xObject);
 343:                             break;
 344: 
 345:                         case Types.DATE:
 346:                         case Types.TIME:
 347:                         case Types.TIMESTAMP:
 348:                             newRow.add(new Long(((Date) xObject).getTime()));
 349:                             break;
 350:                         case Types.NULL:
 351:                             break;
 352:                         default:
 353:                             System.err.println("Unknown data");
 354:                             columnTypes[column] = Types.NULL;
 355:                             break;
 356:                     }
 357:                 }
 358:                 this.rows.add(newRow);
 359:             }
 360: 
 361:             /// a kludge to make everything work when no rows returned
 362:             if (this.rows.size() == 0) {
 363:                 ArrayList newRow = new ArrayList();
 364:                 for (int column = 0; column < numberOfColumns; column++) {
 365:                     if (columnTypes[column] != Types.NULL) {
 366:                         newRow.add(new Integer(0));
 367:                     }
 368:                 }
 369:                 this.rows.add(newRow);
 370:             }
 371: 
 372:             /// Determine max and min values.
 373:             if (this.rows.size() < 1) {
 374:                 this.maxValue = 0.0;
 375:                 this.minValue = 0.0;
 376:             }
 377:             else {
 378:                 ArrayList row = (ArrayList) this.rows.get(0);
 379:                 this.maxValue = Double.NEGATIVE_INFINITY;
 380:                 this.minValue = Double.POSITIVE_INFINITY;
 381:                 for (int rowNum = 0; rowNum < this.rows.size(); ++rowNum) {
 382:                     row = (ArrayList) this.rows.get(rowNum);
 383:                     for (int column = 1; column < numberOfColumns; column++) {
 384:                         Object testValue = row.get(column);
 385:                         if (testValue != null) {
 386:                             double test = ((Number) testValue).doubleValue();
 387:                         
 388:                             if (test < this.minValue) {
 389:                                 this.minValue = test;
 390:                             }
 391:                             if (test > this.maxValue) {
 392:                                 this.maxValue = test;
 393:                             }
 394:                         }
 395:                     }
 396:                 }
 397:             }
 398: 
 399:             fireDatasetChanged(); // Tell the listeners a new table has arrived.
 400:         }
 401:         finally {
 402:             if (resultSet != null) {
 403:                 try {
 404:                     resultSet.close();
 405:                 }
 406:                 catch (Exception e) {
 407:                     // TODO: is this a good idea?
 408:                 }
 409:             }
 410:             if (statement != null) {
 411:                 try {
 412:                     statement.close();
 413:                 }
 414:                 catch (Exception e) {
 415:                     // TODO: is this a good idea?
 416:                 }
 417:             }
 418:         }
 419: 
 420:     }
 421: 
 422:     /**
 423:      * Returns the x-value for the specified series and item.  The
 424:      * implementation is responsible for ensuring that the x-values are
 425:      * presented in ascending order.
 426:      *
 427:      * @param  seriesIndex  the series (zero-based index).
 428:      * @param  itemIndex  the item (zero-based index).
 429:      *
 430:      * @return The x-value
 431:      *
 432:      * @see XYDataset
 433:      */
 434:     public Number getX(int seriesIndex, int itemIndex) {
 435:         ArrayList row = (ArrayList) this.rows.get(itemIndex);
 436:         return (Number) row.get(0);
 437:     }
 438: 
 439:     /**
 440:      * Returns the y-value for the specified series and item.
 441:      *
 442:      * @param  seriesIndex  the series (zero-based index).
 443:      * @param  itemIndex  the item (zero-based index).
 444:      *
 445:      * @return The yValue value
 446:      *
 447:      * @see XYDataset
 448:      */
 449:     public Number getY(int seriesIndex, int itemIndex) {
 450:         ArrayList row = (ArrayList) this.rows.get(itemIndex);
 451:         return (Number) row.get(seriesIndex + 1);
 452:     }
 453: 
 454:     /**
 455:      * Returns the number of items in the specified series.
 456:      *
 457:      * @param  seriesIndex  the series (zero-based index).
 458:      *
 459:      * @return The itemCount value
 460:      *
 461:      * @see XYDataset
 462:      */
 463:     public int getItemCount(int seriesIndex) {
 464:         return this.rows.size();
 465:     }
 466: 
 467:     /**
 468:      * Returns the number of items in all series.  This method is defined by 
 469:      * the {@link TableXYDataset} interface.
 470:      * 
 471:      * @return The item count.
 472:      */
 473:     public int getItemCount() {
 474:         return getItemCount(0);
 475:     }
 476:     
 477:     /**
 478:      * Returns the number of series in the dataset.
 479:      *
 480:      * @return The seriesCount value
 481:      *
 482:      * @see XYDataset
 483:      * @see Dataset
 484:      */
 485:     public int getSeriesCount() {
 486:         return this.columnNames.length;
 487:     }
 488: 
 489:     /**
 490:      * Returns the key for the specified series.
 491:      *
 492:      * @param seriesIndex  the series (zero-based index).
 493:      *
 494:      * @return The seriesName value
 495:      *
 496:      * @see XYDataset
 497:      * @see Dataset
 498:      */
 499:     public Comparable getSeriesKey(int seriesIndex) {
 500: 
 501:         if ((seriesIndex < this.columnNames.length) 
 502:                 && (this.columnNames[seriesIndex] != null)) {
 503:             return this.columnNames[seriesIndex];
 504:         }
 505:         else {
 506:             return "";
 507:         }
 508: 
 509:     }
 510: 
 511:     /**
 512:      * Returns the number of items that should be displayed in the legend.
 513:      *
 514:      * @return The legendItemCount value
 515:      *
 516:      * @deprecated This method is not used in JFreeChart 1.0.x (it was left in
 517:      *     the API by mistake and is officially deprecated from version 1.0.3
 518:      *     onwards).
 519:      */
 520:     public int getLegendItemCount() {
 521:         return getSeriesCount();
 522:     }
 523: 
 524:     /**
 525:      * Returns the legend item labels.
 526:      *
 527:      * @return The legend item labels.
 528:      *
 529:      * @deprecated This method is not used in JFreeChart 1.0.x (it was left in
 530:      *     the API by mistake and is officially deprecated from version 1.0.3
 531:      *     onwards).
 532:      */
 533:     public String[] getLegendItemLabels() {
 534:         return this.columnNames;
 535:     }
 536: 
 537:     /**
 538:      * Close the database connection
 539:      */
 540:     public void close() {
 541: 
 542:         try {
 543:             this.connection.close();
 544:         }
 545:         catch (Exception e) {
 546:             System.err.println("JdbcXYDataset: swallowing exception.");
 547:         }
 548: 
 549:     }
 550: 
 551:     /**
 552:      * Returns the minimum y-value in the dataset.
 553:      *
 554:      * @param includeInterval  a flag that determines whether or not the
 555:      *                         y-interval is taken into account.
 556:      * 
 557:      * @return The minimum value.
 558:      */
 559:     public double getRangeLowerBound(boolean includeInterval) {
 560:         return this.minValue;
 561:     }
 562:     
 563:     /**
 564:      * Returns the maximum y-value in the dataset.
 565:      *
 566:      * @param includeInterval  a flag that determines whether or not the
 567:      *                         y-interval is taken into account.
 568:      * 
 569:      * @return The maximum value.
 570:      */
 571:     public double getRangeUpperBound(boolean includeInterval) {
 572:         return this.maxValue;
 573:     }
 574: 
 575:     /**
 576:      * Returns the range of the values in this dataset's range.
 577:      *
 578:      * @param includeInterval  a flag that determines whether or not the
 579:      *                         y-interval is taken into account.
 580:      * 
 581:      * @return The range.
 582:      */
 583:     public Range getRangeBounds(boolean includeInterval) {
 584:         return new Range(this.minValue, this.maxValue);
 585:     }
 586: 
 587: }