/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.modules.misc.datafactory.sql; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import javax.swing.table.AbstractTableModel; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; import org.pentaho.reporting.engine.classic.core.MetaTableModel; import org.pentaho.reporting.engine.classic.core.modules.misc.tablemodel.DataTableException; import org.pentaho.reporting.engine.classic.core.modules.misc.tablemodel.ImmutableTableMetaData; import org.pentaho.reporting.engine.classic.core.modules.misc.tablemodel.TableMetaData; import org.pentaho.reporting.engine.classic.core.modules.misc.tablemodel.TypeMapper; import org.pentaho.reporting.engine.classic.core.util.CloseableTableModel; import org.pentaho.reporting.engine.classic.core.wizard.DataAttributes; import org.pentaho.reporting.engine.classic.core.wizard.EmptyDataAttributes; import org.pentaho.reporting.engine.classic.core.wizard.ImmutableDataAttributes; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.xmlns.common.AttributeMap; /** * A tableModel which is backed up by a java.sql.ResultSet. Use this to directly feed your database data into * JFreeReport. If you have trouble using this TableModel and you have either enough memory or your query result is not * huge, you may want to use <code>ResultSetTableModelFactory.generateDefaultTableModel (ResultSet rs)</code>. That * implementation will read all data from the given ResultSet and keep that data in memory. * <p/> * Use the close() function to close the ResultSet contained in this model. * * @author Thomas Morgner */ public class ScrollableResultSetTableModel extends AbstractTableModel implements CloseableTableModel, MetaTableModel { public static final String COL_MAPPING_KEY = "org.pentaho.reporting.engine.classic.core.modules.misc.datafactory.sql.ColumnMappingMode"; /** * The scrollable ResultSet source. */ private ResultSet resultset; /** * The ResultSetMetaData object for this result set. */ private ResultSetMetaData dbmd; /** * The number of rows in the result set. */ private int rowCount; /** * Defines the column naming mode. */ private final boolean columnNameMapping; private boolean closeStatement; /** * The column types as read from the result set. */ private Class[] types; private TableMetaData metaData; /** * Constructs the model. * * @param resultset the result set. * @param columnNameMapping defines, whether to use column names or column labels to compute the column index. * @param closeStatement a flag indicating whether the statement, that created the resultset should be closed when * the resultset gets closed. * @throws SQLException if there is a problem with the result set. */ public ScrollableResultSetTableModel( final ResultSet resultset, final boolean columnNameMapping, final boolean closeStatement ) throws SQLException { this.columnNameMapping = columnNameMapping; this.closeStatement = closeStatement; this.rowCount = -1; if ( resultset != null ) { updateResultSet( resultset ); } else { close(); } } /** * Returns the column name mode used to map column names into column indices. If true, then the Name is used, else the * Label is used. * * @return true, if the column name is used for the mapping, false otherwise. * @see ResultSetMetaData#getColumnLabel * @see ResultSetMetaData#getColumnName */ public boolean isColumnNameMapping() { return columnNameMapping; } /** * Updates the result set in this model with the given ResultSet object. * * @param resultset the new result set. * @throws SQLException if there is a problem with the result set. */ public void updateResultSet( final ResultSet resultset ) throws SQLException { if ( this.resultset != null ) { close(); } this.resultset = resultset; this.dbmd = resultset.getMetaData(); final int colcount = dbmd.getColumnCount(); AttributeMap<Object>[] columnMeta = new AttributeMap[colcount]; for ( int i = 0; i < colcount; i++ ) { columnMeta[i] = ResultSetTableModelFactory.collectData( dbmd, i, getColumnName( i ) ); } this.metaData = new ImmutableTableMetaData( ImmutableDataAttributes.EMPTY, ResultSetTableModelFactory.map( columnMeta ) ); if ( resultset.last() ) { rowCount = resultset.getRow(); } else { rowCount = 0; } fireTableStructureChanged(); } /** * Clears the model of the current result set. The resultset is closed. */ public void close() { // Close the old result set if needed. if ( resultset != null ) { Statement statement = null; try { statement = resultset.getStatement(); } catch ( SQLException sqle ) { // yeah, whatever // logger.warn("Failed to close statement", sqle); } try { resultset.close(); } catch ( SQLException e ) { // Just in case the JDBC driver can't close a result set twice. // e.printStackTrace(); // Closing is fine if it fails .. } if ( closeStatement ) { try { if ( statement != null ) { statement.close(); } } catch ( SQLException sqle ) { // yeah, whatever } } } resultset = null; dbmd = null; rowCount = 0; fireTableStructureChanged(); } /** * Get a rowCount. This can be a very expensive operation on large datasets. Returns -1 if the total amount of rows is * not known to the result set. * * @return the row count. */ public int getRowCount() { if ( resultset == null ) { return 0; } if ( rowCount > -1 ) { return rowCount; } try { if ( resultset.last() ) { rowCount = resultset.getRow(); if ( rowCount == -1 ) { rowCount = 0; } } else { rowCount = 0; } } catch ( SQLException sqle ) { //Log.debug ("GetRowCount failed, returning 0 rows", sqle); throw new DataTableException( "Accessing the result set failed: ", sqle ); } return rowCount; } /** * Returns the number of columns in the ResultSet. Returns 0 if no result set is set or the column count could not be * retrieved. * * @return the column count. * @see java.sql.ResultSetMetaData#getColumnCount() */ public int getColumnCount() { if ( resultset == null ) { return 0; } if ( dbmd != null ) { try { return dbmd.getColumnCount(); } catch ( SQLException e ) { //Log.debug ("GetColumnCount failed", e); throw new DataTableException( "Accessing the result set failed: ", e ); } } return 0; } /** * Returns the columnLabel or column name for the given column. Whether the label or the name is returned depends on * the label map mode. * * @param column the column index. * @return the column name. * @see java.sql.ResultSetMetaData#getColumnLabel(int) */ public String getColumnName( final int column ) { if ( dbmd != null ) { try { // In past many database drivers were returning same value for column label and column name. So it is // inconsistent // what the database driver will return for column name vs column label. // We have a legacy configuration for this. If set, then if column label is null or empty then return column // name. // Otherwise return column label. // If non-legacy mode, then we return exactly what the JDBC driver returns (label for label, name for name) // without // any interpretation or interpolation. final Configuration globalConfig = ClassicEngineBoot.getInstance().getGlobalConfig(); final boolean useLegacyColumnMapping = "legacy".equalsIgnoreCase( globalConfig.getConfigProperty( COL_MAPPING_KEY, "legacy" ) ); // NON-NLS String columnLabel = dbmd.getColumnLabel( column + 1 ); if ( useLegacyColumnMapping ) { if ( ( columnLabel == null ) || ( columnLabel.isEmpty() ) ) { // We are in legacy mode and column label is either null or empty, we then use column name instead. columnLabel = dbmd.getColumnName( column + 1 ); } return columnLabel; } else { if ( isColumnNameMapping() ) { return dbmd.getColumnName( column + 1 ); } else { return columnLabel; } } } catch ( SQLException e ) { throw new DataTableException( "Accessing the result set failed: ", e ); } } return null; } /** * Returns the value of the specified row and the specified column from within the resultset. * * @param row the row index. * @param column the column index. * @return the value. */ public Object getValueAt( final int row, final int column ) { if ( resultset != null ) { try { resultset.absolute( row + 1 ); return resultset.getObject( column + 1 ); } catch ( SQLException e ) { throw new DataTableException( "Accessing the result set failed: ", e ); } } return null; } /** * Returns the class of the resultset column. Returns Object.class if an error occurred. * * @param column the column index. * @return the column class. */ public Class getColumnClass( final int column ) { if ( types != null ) { return types[ column ]; } if ( dbmd != null ) { try { types = TypeMapper.mapTypes( dbmd ); if ( types != null ) { return types[ column ]; } } catch ( Exception e ) { throw new DataTableException( "Accessing the result set failed: ", e ); } } return Object.class; } /** * Returns the meta-attribute as Java-Object. The object type that is expected by the report engine is defined in the * TableMetaData property set. It is the responsibility of the implementor to map the native meta-data model into a * model suitable for reporting. * <p/> * Meta-data models that only describe meta-data for columns can ignore the row-parameter. * * @param row the row of the cell for which the meta-data is queried. * @param column the index of the column for which the meta-data is queried. * @return the meta-data object. */ public DataAttributes getCellDataAttributes( final int row, final int column ) { if ( metaData == null ) { return EmptyDataAttributes.INSTANCE; } return metaData.getCellDataAttribute( row, column ); } public boolean isCellDataAttributesSupported() { return metaData.isCellDataAttributesSupported(); } public DataAttributes getColumnAttributes( final int column ) { if ( metaData == null ) { return EmptyDataAttributes.INSTANCE; } return metaData.getColumnAttribute( column ); } /** * Returns table-wide attributes. This usually contain hints about the data-source used to query the data as well as * hints on the sort-order of the data. * * @return */ public DataAttributes getTableAttributes() { if ( metaData == null ) { return null; } return metaData.getTableAttribute(); } }