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