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    
072    package org.jfree.data.jdbc;
073    
074    import java.sql.Connection;
075    import java.sql.DriverManager;
076    import java.sql.ResultSet;
077    import java.sql.ResultSetMetaData;
078    import java.sql.SQLException;
079    import java.sql.Statement;
080    import java.sql.Types;
081    import java.util.ArrayList;
082    import java.util.Date;
083    
084    import org.jfree.data.Range;
085    import org.jfree.data.RangeInfo;
086    import org.jfree.data.general.Dataset;
087    import org.jfree.data.xy.AbstractXYDataset;
088    import org.jfree.data.xy.TableXYDataset;
089    import org.jfree.data.xy.XYDataset;
090    import 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     */
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    }