001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2006, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it 
010 * under the terms of the GNU Lesser General Public License as published by 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 *
027 * ------------------
028 * JDBCXYDataset.java
029 * ------------------
030 * (C) Copyright 2002-2006, by Bryan Scott and Contributors.
031 *
032 * Original Author:  Bryan Scott;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *                   Eric Alexander;
035 *
036 *
037 * Changes
038 * -------
039 * 14-Mar-2002 : Version 1 contributed by Bryan Scott (DG);
040 * 19-Apr-2002 : Updated executeQuery, to close cursors and to improve support 
041 *               for types.
042 * 26-Apr-2002 : Renamed JdbcXYDataset to better fit in with the existing data 
043 *               source conventions.
044 * 26-Apr-2002 : Changed to extend AbstractDataset.
045 * 13-Aug-2002 : Updated Javadoc comments and imports (DG);
046 * 18-Sep-2002 : Updated to support BIGINT (BS);
047 * 21-Jan-2003 : Renamed JdbcXYDataset --> JDBCXYDataset (DG);
048 * 01-Jul-2003 : Added support to query whether a timeseries (BS);
049 * 30-Jul-2003 : Added empty contructor and executeQuery(connection,string) 
050 *               method (BS);
051 * 24-Sep-2003 : Added a check to ensure at least two valid columns are 
052 *               returned by the query in executeQuery as suggest in online 
053 *               forum by anonymous (BS);
054 * 02-Dec-2003 : Throwing exceptions allows to handle errors, removed default 
055 *               constructor, as without a connection, a query can never be 
056 *               executed.
057 * 16-Mar-2004 : Added check for null values (EA);
058 * 05-May-2004 : Now extends AbstractXYDataset (DG);
059 * 21-May-2004 : Implemented TableXYDataset, added support for SMALLINT and 
060 *               fixed bug in code that determines the min and max values (see 
061 *               bug id 938138) (DG);
062 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
063 *               getYValue() (DG);
064 * 18-Nov-2004 : Updated for changes in RangeInfo interface (DG);
065 * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 
066 *               release (DG);
067 * ------------- JFREECHART 1.0.x ---------------------------------------------
068 * 17-Oct-2006 : Deprecated unused methods - see bug 1578293 (DG);
069 * 
070 */
071
072package org.jfree.data.jdbc;
073
074import java.sql.Connection;
075import java.sql.DriverManager;
076import java.sql.ResultSet;
077import java.sql.ResultSetMetaData;
078import java.sql.SQLException;
079import java.sql.Statement;
080import java.sql.Types;
081import java.util.ArrayList;
082import java.util.Date;
083
084import org.jfree.data.Range;
085import org.jfree.data.RangeInfo;
086import org.jfree.data.general.Dataset;
087import org.jfree.data.xy.AbstractXYDataset;
088import org.jfree.data.xy.TableXYDataset;
089import org.jfree.data.xy.XYDataset;
090import org.jfree.util.Log;
091
092/**
093 * This class provides an {@link XYDataset} implementation over a database 
094 * JDBC result set.  The dataset is populated via a call to executeQuery with 
095 * the string sql query.  The sql query must return at least two columns.  
096 * The first column will be the x-axis and remaining columns y-axis values.
097 * executeQuery can be called a number of times.
098 *
099 * The database connection is read-only and no write back facility exists.
100 */
101public 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}