/*
* Copyright (c) 2011 Patrick Meyer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.itemanalysis.jmetrik.model;
import com.itemanalysis.jmetrik.dao.DatabaseAccessObject;
import com.itemanalysis.jmetrik.sql.DataTableName;
import com.itemanalysis.jmetrik.workspace.StatusNotifier;
import com.itemanalysis.psychometrics.data.VariableName;
import com.itemanalysis.squiggle.base.SelectQuery;
import com.itemanalysis.squiggle.base.Table;
import org.apache.log4j.Logger;
import javax.swing.table.AbstractTableModel;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.TreeMap;
import java.util.concurrent.*;
/**
* Model for viewing data. It loads pages of data in a separate thread.
*
* It is almost database independent. The updateData() method
* includes a SelectQuery object that is sepcific to Apache Derby. It needs to be fixed.
*
* In Workspace.java, this model is reloaded anytime a variable is added or deleted.
*
*
*/
public class PagingDataModel extends AbstractTableModel implements StatusNotifier{
/**
* Number of rows of data to hold in memory
*/
private int rowsPerPage = 800;
private int halfRowsPerPage = rowsPerPage/2;
private int activeOffset = 0;
private int maxRow = rowsPerPage-1;
/**
* keep list of edited rows
*/
private ArrayList<Integer> editedRows = null;
private int absoluteNumberOfRows=0;//total number of rows in table
private int absoluteNumberOfColumns=0;//total number of cols in query
private Object[][] data = null;
private Class[] colClasses = null;
private VariableName[] variableNames = null;
protected DatabaseAccessObject dao = null;
protected Connection conn = null;
protected DataTableName tableName = null;
private ArrayList<PropertyChangeListener> propertyChangeListeners = null;
static Logger logger = Logger.getLogger("jmetrik-logger");
//use thread pool to manage data loading
private ThreadPoolExecutor threadPool = null;
private int threadPoolSize = 1;
private int threadPoolSizeMax = 1;
private int maxQueueSize = 5000;
private long threadKeepAliveTime = 1000;
private final ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(maxQueueSize);
public PagingDataModel(Connection conn, DataTableName tableName, DatabaseAccessObject dao, ArrayList<PropertyChangeListener> propertyChangeListeners){
this.conn = conn;
this.tableName = tableName;
this.dao = dao;
this.propertyChangeListeners = propertyChangeListeners;
threadPool = new ThreadPoolExecutor(threadPoolSize, threadPoolSizeMax, threadKeepAliveTime, TimeUnit.SECONDS, queue);
threadPool.prestartCoreThread();
editedRows = new ArrayList<Integer>();
initialize();
}
private void initialize(){
try{
// Table table = new Table(tableName.getNameForDatabase());
// SelectQuery select = new SelectQuery();
// select.addColumn(table, "*");
absoluteNumberOfRows = dao.getRowCount(conn, tableName);//number of rows in db table
//get number of rows in table -- very slow
// String QUERY = "SELECT COUNT(*) FROM " + tableName.getNameForDatabase();
// Statement stmt = conn.createStatement();
// ResultSet rs = stmt.executeQuery(QUERY);
// rs.next();
// absoluteNumberOfRows = rs.getInt(1);
halfRowsPerPage = rowsPerPage/2;
absoluteNumberOfColumns = dao.getColumnCount(conn, tableName);
data = new Object[rowsPerPage][absoluteNumberOfColumns];
variableNames = dao.getColumnNames(conn, tableName);
colClasses = dao.getColumnClass(conn, tableName);
updateData(0, 0, true);//initial load
}catch(Exception ex){
logger.fatal(ex.getMessage(), ex);
this.firePropertyChange("error", "", "Error - Check log for details.");
}
}
@Override
public Object getValueAt(int r, int c){
checkForRow(r);
return data[r- activeOffset][c];
}
@Override
public void setValueAt(Object value, int r, int c){
data[r- activeOffset][c] = value;
editedRows.add(r- activeOffset);
}
@Override
public boolean isCellEditable(int r, int c){
return true;
}
@Override
public int getRowCount(){
return absoluteNumberOfRows;
}
@Override
public int getColumnCount(){
return absoluteNumberOfColumns;
}
private void checkForRow(int r){
if(r < activeOffset || r > maxRow){
//set new activeOffset and load more data
int newOffset = Math.max(r-halfRowsPerPage, 0);//TODO could be a problem if scroll fast than load?
updateData(activeOffset, newOffset, true);
}
}
private void updateData(int saveOffset, int loadOffset, boolean loadData){
SaveLoadDataProcess saveLoad = new SaveLoadDataProcess(saveOffset, loadOffset, loadData);
try{
int result = threadPool.submit(saveLoad).get(); //should block until thread completes work
// this.firePropertyChange("status", "", "Ready");
// int result = threadPool.submit(saveLoad).get();
if(result==-1){
//only save happened
// this.firePropertyChange("status", "", "Data saved.");
}else{
//edits saved and new data loaded
activeOffset = result;
maxRow = activeOffset +rowsPerPage-1;
}
}catch(InterruptedException ex){
logger.fatal(ex.getMessage(), ex);
this.firePropertyChange("error", "", "Error - Check log for details.");
}catch(Exception ex){
logger.fatal(ex.getMessage(), ex);
this.firePropertyChange("error", "", "Error - Check log for details.");
}
}
public void saveData(){
updateData(activeOffset, activeOffset, false);
}
public boolean dataEdited(){
return !editedRows.isEmpty();
}
@Override
public Class getColumnClass(int col){
return colClasses[col];
}
@Override
public String getColumnName(int col){
if(col<variableNames.length)return variableNames[col].toString();
return "Var" + col;
}
//===============================================================================================================
//Process messages here
//===============================================================================================================
public synchronized void addPropertyChangeListener(PropertyChangeListener l){
propertyChangeListeners.add(l);
}
public synchronized void removePropertyChangeListener(PropertyChangeListener l){
propertyChangeListeners.remove(l);
}
public synchronized void firePropertyChange(String propertyName, Object oldValue, Object newValue){
PropertyChangeEvent e = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
for(PropertyChangeListener l : propertyChangeListeners){
l.propertyChange(e);
}
}
//===============================================================================================================
/**
* Class that saves and loads data.
* If no edits have been made to the data, then nothing will be saved.
*
*/
class SaveLoadDataProcess implements Callable<Integer>{
private int saveOffset = 0;
private int loadOffset = 0;
private boolean loadData = false;
public SaveLoadDataProcess(int saveOffset, int loadOffset, boolean loadData){
this.saveOffset = saveOffset;
this.loadOffset = loadOffset;
this.loadData = loadData;
}
/**
* Create tree map where rows contain integers of rows numbers using the
* rows number of the target database. The values are the rows of
* data that have been edited
*/
private TreeMap<Integer, Object[]> configureData(){
TreeMap<Integer, Object[]> dataToSave = new TreeMap<Integer, Object[]>();
for(Integer i : editedRows){
dataToSave.put(i+1, data[i]);//database start index at 1
}
return dataToSave;
}
public void saveData()throws Exception{
Table table = new Table(tableName.getNameForDatabase());
SelectQuery select = new SelectQuery();
select.addColumn(table, "*");
select.addOffset(saveOffset, rowsPerPage);//TODO this activeOffset and limit is specific to derby
dao.saveData(conn, select, configureData());
editedRows.clear();
}
public Integer loadData()throws Exception{
Table table = new Table(tableName.getNameForDatabase());
SelectQuery select = new SelectQuery();
select.addColumn(table, "*");
select.addOffset(loadOffset, rowsPerPage);//TODO this activeOffset and limit is specific to derby
data = dao.getData(conn, select, rowsPerPage, absoluteNumberOfColumns);
//display data for debugging
// for(int i=0;i<data.length;i++){
// for(int j=0;j<data[0].length; j++){
// System.out.print(data[i][j] + " ");
// }
// System.out.println();
// }
return loadOffset;
}
public Integer call()throws Exception{
if(editedRows.size()>0) saveData();
if(loadData) return loadData();
return -1;
}
}
}