001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2013, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ------------------
028 * JDBCXYDataset.java
029 * ------------------
030 * (C) Copyright 2002-2009, 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 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG);
070 *
071 */
072
073package org.jfree.data.jdbc;
074
075import java.sql.Connection;
076import java.sql.DriverManager;
077import java.sql.ResultSet;
078import java.sql.ResultSetMetaData;
079import java.sql.SQLException;
080import java.sql.Statement;
081import java.sql.Types;
082import java.util.ArrayList;
083import java.util.Date;
084
085import org.jfree.data.Range;
086import org.jfree.data.RangeInfo;
087import org.jfree.data.general.Dataset;
088import org.jfree.data.xy.AbstractXYDataset;
089import org.jfree.data.xy.TableXYDataset;
090import org.jfree.data.xy.XYDataset;
091import org.jfree.util.Log;
092
093/**
094 * This class provides an {@link XYDataset} implementation over a database
095 * JDBC result set.  The dataset is populated via a call to executeQuery with
096 * the string sql query.  The sql query must return at least two columns.
097 * The first column will be the x-axis and remaining columns y-axis values.
098 * executeQuery can be called a number of times.
099 *
100 * The database connection is read-only and no write back facility exists.
101 */
102public class JDBCXYDataset extends AbstractXYDataset
103        implements XYDataset, TableXYDataset, RangeInfo {
104
105    /** The database connection. */
106    private transient Connection connection;
107
108    /** Column names. */
109    private String[] columnNames = {};
110
111    /** Rows. */
112    private ArrayList rows;
113
114    /** The maximum y value of the returned result set */
115    private double maxValue = 0.0;
116
117    /** The minimum y value of the returned result set */
118    private double minValue = 0.0;
119
120    /** Is this dataset a timeseries ? */
121    private boolean isTimeSeries = false;
122
123    /**
124     * Creates a new JDBCXYDataset (initially empty) with no database
125     * connection.
126     */
127    private JDBCXYDataset() {
128        this.rows = new ArrayList();
129    }
130
131    /**
132     * Creates a new dataset (initially empty) and establishes a new database
133     * connection.
134     *
135     * @param  url  URL of the database connection.
136     * @param  driverName  the database driver class name.
137     * @param  user  the database user.
138     * @param  password  the database user's password.
139     *
140     * @throws ClassNotFoundException if the driver cannot be found.
141     * @throws SQLException if there is a problem connecting to the database.
142     */
143    public JDBCXYDataset(String url,
144                         String driverName,
145                         String user,
146                         String password)
147        throws SQLException, ClassNotFoundException {
148
149        this();
150        Class.forName(driverName);
151        this.connection = DriverManager.getConnection(url, user, password);
152    }
153
154    /**
155     * Creates a new dataset (initially empty) using the specified database
156     * connection.
157     *
158     * @param  con  the database connection.
159     *
160     * @throws SQLException if there is a problem connecting to the database.
161     */
162    public JDBCXYDataset(Connection con) throws SQLException {
163        this();
164        this.connection = con;
165    }
166
167    /**
168     * Creates a new dataset using the specified database connection, and
169     * populates it using data obtained with the supplied query.
170     *
171     * @param con  the connection.
172     * @param query  the SQL query.
173     *
174     * @throws SQLException if there is a problem executing the query.
175     */
176    public JDBCXYDataset(Connection con, String query) throws SQLException {
177        this(con);
178        executeQuery(query);
179    }
180
181    /**
182     * Returns <code>true</code> if the dataset represents time series data,
183     * and <code>false</code> otherwise.
184     *
185     * @return A boolean.
186     */
187    public boolean isTimeSeries() {
188        return this.isTimeSeries;
189    }
190
191    /**
192     * Sets a flag that indicates whether or not the data represents a time
193     * series.
194     *
195     * @param timeSeries  the new value of the flag.
196     */
197    public void setTimeSeries(boolean timeSeries) {
198        this.isTimeSeries = timeSeries;
199    }
200
201    /**
202     * ExecuteQuery will attempt execute the query passed to it against the
203     * existing database connection.  If no connection exists then no action
204     * is taken.
205     *
206     * The results from the query are extracted and cached locally, thus
207     * applying an upper limit on how many rows can be retrieved successfully.
208     *
209     * @param  query  the query to be executed.
210     *
211     * @throws SQLException if there is a problem executing the query.
212     */
213    public void executeQuery(String query) throws SQLException {
214        executeQuery(this.connection, query);
215    }
216
217    /**
218     * ExecuteQuery will attempt execute the query passed to it against the
219     * provided database connection.  If connection is null then no action is
220     * taken.
221     *
222     * The results from the query are extracted and cached locally, thus
223     * applying an upper limit on how many rows can be retrieved successfully.
224     *
225     * @param  query  the query to be executed.
226     * @param  con  the connection the query is to be executed against.
227     *
228     * @throws SQLException if there is a problem executing the query.
229     */
230    public void executeQuery(Connection con, String query)
231        throws SQLException {
232
233        if (con == null) {
234            throw new SQLException(
235                "There is no database to execute the query."
236            );
237        }
238
239        ResultSet resultSet = null;
240        Statement statement = null;
241        try {
242            statement = con.createStatement();
243            resultSet = statement.executeQuery(query);
244            ResultSetMetaData metaData = resultSet.getMetaData();
245
246            int numberOfColumns = metaData.getColumnCount();
247            int numberOfValidColumns = 0;
248            int [] columnTypes = new int[numberOfColumns];
249            for (int column = 0; column < numberOfColumns; column++) {
250                try {
251                    int type = metaData.getColumnType(column + 1);
252                    switch (type) {
253
254                        case Types.NUMERIC:
255                        case Types.REAL:
256                        case Types.INTEGER:
257                        case Types.DOUBLE:
258                        case Types.FLOAT:
259                        case Types.DECIMAL:
260                        case Types.BIT:
261                        case Types.DATE:
262                        case Types.TIME:
263                        case Types.TIMESTAMP:
264                        case Types.BIGINT:
265                        case Types.SMALLINT:
266                            ++numberOfValidColumns;
267                            columnTypes[column] = type;
268                            break;
269                        default:
270                            Log.warn(
271                                "Unable to load column "
272                                + column + " (" + type + ","
273                                + metaData.getColumnClassName(column + 1)
274                                + ")"
275                            );
276                            columnTypes[column] = Types.NULL;
277                            break;
278                    }
279                }
280                catch (SQLException e) {
281                    columnTypes[column] = Types.NULL;
282                    throw e;
283                }
284            }
285
286
287            if (numberOfValidColumns <= 1) {
288                throw new SQLException(
289                    "Not enough valid columns where generated by query."
290                );
291            }
292
293            /// First column is X data
294            this.columnNames = new String[numberOfValidColumns - 1];
295            /// Get the column names and cache them.
296            int currentColumn = 0;
297            for (int column = 1; column < numberOfColumns; column++) {
298                if (columnTypes[column] != Types.NULL) {
299                    this.columnNames[currentColumn]
300                        = metaData.getColumnLabel(column + 1);
301                    ++currentColumn;
302                }
303            }
304
305            // Might need to add, to free memory from any previous result sets
306            if (this.rows != null) {
307                for (int column = 0; column < this.rows.size(); column++) {
308                    ArrayList row = (ArrayList) this.rows.get(column);
309                    row.clear();
310                }
311                this.rows.clear();
312            }
313
314            // Are we working with a time series.
315            switch (columnTypes[0]) {
316                case Types.DATE:
317                case Types.TIME:
318                case Types.TIMESTAMP:
319                    this.isTimeSeries = true;
320                    break;
321                default :
322                    this.isTimeSeries = false;
323                    break;
324            }
325
326            // Get all rows.
327            // rows = new ArrayList();
328            while (resultSet.next()) {
329                ArrayList newRow = new ArrayList();
330                for (int column = 0; column < numberOfColumns; column++) {
331                    Object xObject = resultSet.getObject(column + 1);
332                    switch (columnTypes[column]) {
333                        case Types.NUMERIC:
334                        case Types.REAL:
335                        case Types.INTEGER:
336                        case Types.DOUBLE:
337                        case Types.FLOAT:
338                        case Types.DECIMAL:
339                        case Types.BIGINT:
340                        case Types.SMALLINT:
341                            newRow.add(xObject);
342                            break;
343
344                        case Types.DATE:
345                        case Types.TIME:
346                        case Types.TIMESTAMP:
347                            newRow.add(new Long(((Date) xObject).getTime()));
348                            break;
349                        case Types.NULL:
350                            break;
351                        default:
352                            System.err.println("Unknown data");
353                            columnTypes[column] = Types.NULL;
354                            break;
355                    }
356                }
357                this.rows.add(newRow);
358            }
359
360            /// a kludge to make everything work when no rows returned
361            if (this.rows.isEmpty()) {
362                ArrayList newRow = new ArrayList();
363                for (int column = 0; column < numberOfColumns; column++) {
364                    if (columnTypes[column] != Types.NULL) {
365                        newRow.add(new Integer(0));
366                    }
367                }
368                this.rows.add(newRow);
369            }
370
371            /// Determine max and min values.
372            if (this.rows.size() < 1) {
373                this.maxValue = 0.0;
374                this.minValue = 0.0;
375            }
376            else {
377                this.maxValue = Double.NEGATIVE_INFINITY;
378                this.minValue = Double.POSITIVE_INFINITY;
379                for (int rowNum = 0; rowNum < this.rows.size(); ++rowNum) {
380                    ArrayList row = (ArrayList) this.rows.get(rowNum);
381                    for (int column = 1; column < numberOfColumns; column++) {
382                        Object testValue = row.get(column);
383                        if (testValue != null) {
384                            double test = ((Number) testValue).doubleValue();
385
386                            if (test < this.minValue) {
387                                this.minValue = test;
388                            }
389                            if (test > this.maxValue) {
390                                this.maxValue = test;
391                            }
392                        }
393                    }
394                }
395            }
396
397            fireDatasetChanged(); // Tell the listeners a new table has arrived.
398        }
399        finally {
400            if (resultSet != null) {
401                try {
402                    resultSet.close();
403                }
404                catch (Exception e) {
405                    // TODO: is this a good idea?
406                }
407            }
408            if (statement != null) {
409                try {
410                    statement.close();
411                }
412                catch (Exception e) {
413                    // TODO: is this a good idea?
414                }
415            }
416        }
417
418    }
419
420    /**
421     * Returns the x-value for the specified series and item.  The
422     * implementation is responsible for ensuring that the x-values are
423     * presented in ascending order.
424     *
425     * @param  seriesIndex  the series (zero-based index).
426     * @param  itemIndex  the item (zero-based index).
427     *
428     * @return The x-value
429     *
430     * @see XYDataset
431     */
432    @Override
433    public Number getX(int seriesIndex, int itemIndex) {
434        ArrayList row = (ArrayList) this.rows.get(itemIndex);
435        return (Number) row.get(0);
436    }
437
438    /**
439     * Returns the y-value for the specified series and item.
440     *
441     * @param  seriesIndex  the series (zero-based index).
442     * @param  itemIndex  the item (zero-based index).
443     *
444     * @return The yValue value
445     *
446     * @see XYDataset
447     */
448    @Override
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    @Override
464    public int getItemCount(int seriesIndex) {
465        return this.rows.size();
466    }
467
468    /**
469     * Returns the number of items in all series.  This method is defined by
470     * the {@link TableXYDataset} interface.
471     *
472     * @return The item count.
473     */
474    @Override
475    public int getItemCount() {
476        return getItemCount(0);
477    }
478
479    /**
480     * Returns the number of series in the dataset.
481     *
482     * @return The seriesCount value
483     *
484     * @see XYDataset
485     * @see Dataset
486     */
487    @Override
488    public int getSeriesCount() {
489        return this.columnNames.length;
490    }
491
492    /**
493     * Returns the key for the specified series.
494     *
495     * @param seriesIndex  the series (zero-based index).
496     *
497     * @return The seriesName value
498     *
499     * @see XYDataset
500     * @see Dataset
501     */
502    @Override
503    public Comparable getSeriesKey(int seriesIndex) {
504
505        if ((seriesIndex < this.columnNames.length)
506                && (this.columnNames[seriesIndex] != null)) {
507            return this.columnNames[seriesIndex];
508        }
509        else {
510            return "";
511        }
512
513    }
514
515    /**
516     * Returns the number of items that should be displayed in the legend.
517     *
518     * @return The legendItemCount value
519     *
520     * @deprecated This method is not used in JFreeChart 1.0.x (it was left in
521     *     the API by mistake and is officially deprecated from version 1.0.3
522     *     onwards).
523     */
524    public int getLegendItemCount() {
525        return getSeriesCount();
526    }
527
528    /**
529     * Returns the legend item labels.
530     *
531     * @return The legend item labels.
532     *
533     * @deprecated This method is not used in JFreeChart 1.0.x (it was left in
534     *     the API by mistake and is officially deprecated from version 1.0.3
535     *     onwards).
536     */
537    public String[] getLegendItemLabels() {
538        return this.columnNames;
539    }
540
541    /**
542     * Close the database connection
543     */
544    public void close() {
545
546        try {
547            this.connection.close();
548        }
549        catch (Exception e) {
550            System.err.println("JdbcXYDataset: swallowing exception.");
551        }
552
553    }
554
555    /**
556     * Returns the minimum y-value in the dataset.
557     *
558     * @param includeInterval  a flag that determines whether or not the
559     *                         y-interval is taken into account.
560     *
561     * @return The minimum value.
562     */
563    @Override
564    public double getRangeLowerBound(boolean includeInterval) {
565        return this.minValue;
566    }
567
568    /**
569     * Returns the maximum y-value in the dataset.
570     *
571     * @param includeInterval  a flag that determines whether or not the
572     *                         y-interval is taken into account.
573     *
574     * @return The maximum value.
575     */
576    @Override
577    public double getRangeUpperBound(boolean includeInterval) {
578        return this.maxValue;
579    }
580
581    /**
582     * Returns the range of the values in this dataset's range.
583     *
584     * @param includeInterval  a flag that determines whether or not the
585     *                         y-interval is taken into account.
586     *
587     * @return The range.
588     */
589    @Override
590    public Range getRangeBounds(boolean includeInterval) {
591        return new Range(this.minValue, this.maxValue);
592    }
593
594}