/* * Copyright 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.workspace; import com.itemanalysis.jmetrik.commandbuilder.Command; import com.itemanalysis.jmetrik.commandbuilder.TextToCommand; import com.itemanalysis.jmetrik.dao.*; import com.itemanalysis.jmetrik.swing.DataTable; import com.itemanalysis.jmetrik.swing.NumericCellRenderer; import com.itemanalysis.jmetrik.model.*; import com.itemanalysis.jmetrik.sql.DataTableName; import com.itemanalysis.jmetrik.sql.DatabaseName; import com.itemanalysis.jmetrik.sql.VariableTableName; import com.itemanalysis.psychometrics.data.VariableAttributes; import org.apache.log4j.Logger; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.TableModel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.sql.*; import java.util.ArrayList; import java.util.Iterator; import java.util.Properties; import java.util.ServiceLoader; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Workspace implements StatusNotifier{ private JmetrikDatabaseFactory dbFactory = null; private DatabaseAccessObject dao = null; private Connection conn = null; private JList workspaceList = null; private SortedListModel<DataTableName> tableListModel = null; private DataTable dataTable = null; private DataTable variableTable = null; private ArrayList<PropertyChangeListener> propertyChangeListeners = null; private DatabaseName currentDbName = null; private DataTableName currentDataTable = null; private VariableTableName currentVariableTable = null; private DatabaseConnectionURL databaseConnectionURL = null; private JmetrikPreferencesManager prefs = null; private JmetrikProcessFactory procFactory = null; private boolean databaseOpened = false; private JTabbedPane tabbedPane = null; private String dbHome = ""; private ArrayList<VariableChangeListener> variableChangeListeners = null; private ThreadPoolExecutor threadPool = null; private int threadPoolSize = 1; private int threadPoolSizeMax = 1; private int maxQueueSize = 5000; private long threadKeepAliveTime = 10; private final ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(maxQueueSize); static Logger logger = Logger.getLogger("jmetrik-logger"); private JButton refreshButton = null; public Workspace(final JList workspaceList, JTabbedPane tabbedPane, DataTable dataTable, DataTable variableTable){ this.workspaceList = workspaceList; this.tabbedPane = tabbedPane; this.dataTable = dataTable; this.variableTable = variableTable; threadPool = new ThreadPoolExecutor(threadPoolSize, threadPoolSizeMax, threadKeepAliveTime, TimeUnit.SECONDS, queue); threadPool.prestartCoreThread(); workspaceList.addListSelectionListener(new JmetrikListSelectionListener()); tableListModel = new SortedListModel<DataTableName>(); workspaceList.setModel(tableListModel); //set model for data table dataTable.setModel(new EmptyTableModel()); dataTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); //set model for variable table variableTable.setModel(new EmptyVariableModel()); // variableTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); //create property and variable listener lists propertyChangeListeners = new ArrayList<PropertyChangeListener>(); variableChangeListeners = new ArrayList<VariableChangeListener>(); //load properties loadProperties(); procFactory = new JmetrikProcessFactory(); } private void loadProperties(){ prefs = new JmetrikPreferencesManager(); for(PropertyChangeListener pcl : propertyChangeListeners){ prefs.addPropertyChangeListener(pcl); } //set type of database according to properties String dbType = prefs.getDatabaseType(); if(DatabaseType.APACHE_DERBY.toString().equals(dbType)){ dao = new DerbyDatabaseAccessObject(); dbFactory = new JmetrikDatabaseFactory(DatabaseType.APACHE_DERBY); }else if(DatabaseType.MYSQL.toString().equals(dbType)){ //not yet implemented }else{ //default is apache derby dao = new DerbyDatabaseAccessObject(); dbFactory = new JmetrikDatabaseFactory(DatabaseType.APACHE_DERBY); } // System.out.println(System.getProperty("derby.system.home"));//returns null dbHome = prefs.getDatabaseHome(); System.setProperty("derby.system.home", dbHome); // System.out.println(System.getProperty("derby.system.home")); int precision = prefs.getPrecision(); dataTable.setDefaultRenderer(Double.class, new NumericCellRenderer(precision)); } /** * Changes to jMetrik often involve changes to the underlying database. This * method checks a property file in the DatabaseHome folder to see if the * databases have been updated. If not, updates are made and the property * is changed. * * */ private void updateDatabaseVersion(){ //create new columns in jmk_table_rows table and all variable tables Properties p = new Properties(); FileInputStream in = null; ResultSet rs = null; boolean mustUpdateDb = false; try{ String dbName = dbHome + "/" + conn.getMetaData().getURL(); dbName = dbName.replaceAll("[\\\\/:]+", "."); dbName = dbName.replaceAll("[.]+", "."); File f = new File(dbHome + "/jmetrik-db-version.props"); if(!f.exists()) f.createNewFile(); in = new FileInputStream(f); p.load(in); in.close(); String currentVersion = p.getProperty(dbName, "jmk-db-not-found"); if(currentVersion==null || "jmk-db-not-found".equals(currentVersion) || !"version3".equals(currentVersion)){ logger.info("Updating database: " + dbName); dao.updateDatabasesVersion(conn); } }catch(SQLException ex){ logger.fatal(ex.getMessage(), ex); firePropertyChange("error", "", "Error - Check log for details."); }catch(IOException ex){ logger.fatal(ex.getMessage(), ex); firePropertyChange("error", "", "Error - Check log for details."); }finally{ try{ if(rs!=null) rs.close(); if(in!=null) in.close(); }catch(Exception ex){ logger.fatal(ex.getMessage(), ex); firePropertyChange("error", "", "Error - Check log for details."); } } } /** * Go to database home and construct a list of all databases * * @param list */ public void setDatabaseListModel(final JList list){ SwingWorker<SortedListModel<DatabaseName>, Void> task = new SwingWorker<SortedListModel<DatabaseName>, Void>(){ protected SortedListModel<DatabaseName> doInBackground()throws Exception{ firePropertyChange("status", "", "Getting database list..."); firePropertyChange("progress-ind-on", null, null); return dao.getDatabaseListModel(prefs.getDatabaseHome()); } protected void done(){ try{ list.setModel(get()); firePropertyChange("status", "", "Ready"); firePropertyChange("progress-off", null, null); }catch(Exception ex){ logger.fatal(ex.getMessage(), ex); firePropertyChange("error", "", "Error - Check log for details."); firePropertyChange("progress-off", null, null); } } }; for(PropertyChangeListener l : propertyChangeListeners){ task.addPropertyChangeListener(l); } threadPool.execute(task); } public void setRefreshButton(JButton refreshButton){ this.refreshButton = refreshButton; } /** * Create a list of database tables. For workspace list * */ public void setTableListModel(){ SwingWorker<SortedListModel<DataTableName>, Void> task = new SwingWorker<SortedListModel<DataTableName>, Void>(){ protected SortedListModel<DataTableName> doInBackground()throws Exception{ firePropertyChange("status", "", "Getting table list..."); firePropertyChange("progress-ind-on", null, null); return dao.getTableListModel(conn); } protected void done(){ try{ tableListModel = get(); workspaceList.setModel(tableListModel); firePropertyChange("status", "", "Ready"); firePropertyChange("progress-off", null, null); }catch(Exception ex){ logger.fatal(ex.getMessage(), ex); firePropertyChange("error", "", "Error - Check log for details."); firePropertyChange("progress-off", null, null); } } }; for(PropertyChangeListener l : propertyChangeListeners){ task.addPropertyChangeListener(l); } threadPool.execute(task); } public String getDatabaseHome(){ return dbHome; } public DatabaseName getDatabaseName(){ return databaseConnectionURL.getName(); } /** * Connect to database and populate database list in worker thread. Note that the * database must already exist. * * * @throws SQLException */ public void openDatabase(String dbName)throws SQLException{ if(conn!=null && !conn.isClosed()){ //committ transactions and close connection to existing database conn.commit(); conn.close(); } this.databaseConnectionURL = dbFactory.getDatabaseConnectionURL(); this.databaseConnectionURL.setDatabaseName(dbName); currentDbName = new DatabaseName(dbName); dataTable.setModel(new EmptyTableModel()); variableTable.setModel(new EmptyVariableModel()); conn = DriverManager.getConnection(databaseConnectionURL.getConnectionUrl()); //check that database structure reflect the current version of software. updateDatabaseVersion(); //populate table list model setTableListModel(); databaseOpened = true; firePropertyChange("db-selection", "", currentDbName.toString()); } /** * This method closes the connection to the database and resets the interface. * Closing is done in the thread pool to allow completion of existing tasks before closing. * * @throws java.sql.SQLException */ public void closeDatabase(){ firePropertyChange("status", "", "Closing workspace..."); try{ TableModel m = variableTable.getModel(); if(m instanceof VariableModel){ ((VariableModel)m).saveData(); } //TODO add saving data table if(conn!=null && !conn.isClosed()){ databaseConnectionURL.setProperty("shutdown", "true"); DriverManager.getConnection(databaseConnectionURL.getConnectionUrl()); databaseConnectionURL = null; conn.close(); resetGUI(); } }catch(SQLException ex){ if((ex.getErrorCode() == 45000) && ("08006".equals(ex.getSQLState()))){ //normal shutdown logger.info("Normal Derby Shutdown: " + this.getDatabaseName()); resetGUI(); databaseOpened = false; }else{ logger.fatal(ex.getMessage(), ex); firePropertyChange("error", "", "Error - Check log for details."); } }finally{ firePropertyChange("status", "", "Ready"); } } private void resetGUI(){ currentDbName = null; databaseConnectionURL = null; dataTable.setModel(new EmptyTableModel()); variableTable.setModel(new EmptyVariableModel()); tableListModel = new SortedListModel<DataTableName>(); workspaceList.setModel(tableListModel); databaseOpened = false; } /** * Returns a connection to the db. The is just the base connection. Connections needed for * creating or shutting down a db are done elsewhere. * * @return * @throws SQLException */ public Connection getConnection() { return conn; } public DataTableName getCurrentDataTable(){ return currentDataTable; } public ArrayList<VariableAttributes> getVariables(){ ArrayList<VariableAttributes> variables = null; try{ variables = dao.getAllVariables(conn, currentVariableTable); }catch(Exception ex){ logger.fatal(ex.getMessage(), ex); firePropertyChange("error", "", "Error - Check log for details."); } return variables; } public boolean tableOpen(){ return currentDataTable!=null; } public boolean tableSelectionChanged(String tableName){ if(currentDataTable==null) return true; DataTableName dName = new DataTableName(tableName); return !currentDataTable.equals(dName); } public void saveTable(){ //save edited data before changing table TableModel tempTable = dataTable.getModel(); if(tempTable instanceof PagingDataModel){ PagingDataModel pm = (PagingDataModel)tempTable; pm.saveData(); } //save variable table edits before changing table TableModel tempModel = variableTable.getModel(); if(tempModel instanceof VariableModel){ VariableModel vm =(VariableModel)tempModel; vm.saveData(); } } /** * Opens table called tableName. * * @param tableName */ public void reloadTable(DataTableName tableName){ if(null==tableName) return; //load data model PagingDataModel dataModel = new PagingDataModel(conn, tableName, dao, propertyChangeListeners); dataTable.setModel(dataModel); currentDataTable = tableName; //Load variable model VariableTableName cVarTable = new VariableTableName(tableName.toString()); VariableModel variableModel = new VariableModel(conn, currentDbName, cVarTable, dao, propertyChangeListeners); for(VariableChangeListener l : variableChangeListeners){ variableModel.addVariableChangeListener(l); } variableTable.setModel(variableModel); currentVariableTable = cVarTable; } /** * Opens table if different from current one or current table is null. * * @param tableName */ public void openTable(DataTableName tableName){ //only open table if different from current one if(currentDataTable!=null && currentDataTable.equals(tableName)) return; DataTableName oldTableName = currentDataTable; saveTable(); reloadTable(tableName); firePropertyChange("table-selection", oldTableName, tableName); firePropertyChange("status", "", "Ready"); } public void loadMoreData(DataTableName tableName){ PagingDataModel dataModel = new PagingDataModel(conn, tableName, dao, propertyChangeListeners); dataTable.setModel(dataModel); } public void openEmptyTable(){ dataTable.setModel(new EmptyTableModel()); currentDataTable = null; variableTable.setModel(new EmptyVariableModel()); currentVariableTable = null; firePropertyChange("status", "", "Ready"); } public boolean databaseOpened(){ return databaseOpened; } /** * This method handles any action that must come before any process is run. * For example, data table and variable table information is saved before * any process is run. * * @param command */ private void runPreProcess(Command command){ //save edited data before changing table saveTable(); //If deleting current database, must first close workspace. DatabaseCommand dbCommand = new DatabaseCommand(); if(dbCommand.equals(command)){ dbCommand = (DatabaseCommand)command; String action = dbCommand.getSelectOneOption("action").getSelectedArgument(); String name = dbCommand.getFreeOption("name").getString(); DatabaseName dbName = new DatabaseName(name); if("delete-db".equals(action)){ if(this.getDatabaseName().toString().equals(dbName.getName())){ this.closeDatabase(); } }else if("delete-table".equals(action)){ ArrayList<String> nameList = command.getFreeOptionList("tables").getString(); DataTableName tempName = null; for(String s : nameList){ tempName = new DataTableName(s); if(currentDataTable!=null && currentDataTable.equals(tempName)){ this.openEmptyTable(); } } } } } public void runFromSyntax(String text){ TextToCommand converter = new TextToCommand(); String commandName = ""; String commandSyntax = ""; JmetrikCommandFactory commandFactory = new JmetrikCommandFactory(); Command command = null; converter.convertToCommands(text); Iterator<String[]> iter = converter.iterator(); String[] sa = null; int count = 0; while(iter.hasNext()){ sa = iter.next(); commandName = sa[0]; commandSyntax = sa[1]; command = commandFactory.getCommand(sa[0], sa[1]); if(command!=null) runProcess(command); } } /** * Run plugin process * * @param command * @param tabbedPane * @return true if process found */ private boolean runPluginProcess(Command command, JTabbedPane tabbedPane){ boolean processFound = false; //run plugin processes ServiceLoader<JmetrikProcess> loader = ServiceLoader.load(JmetrikProcess.class); for(JmetrikProcess proc : loader){ if(proc.commandMatch(command)){ processFound = true; proc.setCommand(command); proc.addVariableChangeListener(new VariableListener()); proc.addPropertyChangeListener(new DatabaseChangeListener()); for(PropertyChangeListener l : propertyChangeListeners){ proc.addPropertyChangeListener(l); } proc.runProcess(conn, dbFactory, tabbedPane, threadPool); break; } } return processFound; } /** * All actions that have a Command object are run from here. * This method runs any necessary pre process, then runs either a base process * or a plugin process. * * @param command */ public void runProcess(Command command){ runPreProcess(command); //run base processes here JmetrikProcess proc = procFactory.getProcess(command); if(proc==null){ //No base process found. Attempt to run plugin. boolean pluginFound = runPluginProcess(command, tabbedPane); if(!pluginFound){ logger.fatal("Process not found: " + command.getName()); firePropertyChange("error", "", "Error - Check log for details."); } }else{ proc.setCommand(command); //add variable change listeners to process and thereby the analysis object proc.addVariableChangeListener(new VariableListener()); for(VariableChangeListener l : variableChangeListeners){ proc.addVariableChangeListener(l); } //add property change listeners to process and thereby the analysis object proc.addPropertyChangeListener(new DatabaseChangeListener()); for(PropertyChangeListener l : propertyChangeListeners){ proc.addPropertyChangeListener(l); } //execute process proc.runProcess(conn, dbFactory, tabbedPane, threadPool); } } public JmetrikDatabaseFactory getDatabaseFactory(){ return dbFactory; } //=============================================================================================================== //Process messages here // Note that SwingWorker classes also implement these methods. Just need to add list of // propertyChangeListeners to SwingWorker classes. See importTable(...) for an example. //=============================================================================================================== 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); } } //=============================================================================================================== //Handle variable changes here // -Dialogs will use these methods to add their variable listeners //=============================================================================================================== public synchronized void addVariableChangeListener(VariableChangeListener l){ TableModel m = variableTable.getModel(); if(m instanceof VariableModel){ VariableModel vModel = (VariableModel)m; vModel.addVariableChangeListener(l); } variableChangeListeners.add(l); } public synchronized void removeVariableChangeListener(VariableChangeListener l){ TableModel m = variableTable.getModel(); if(m instanceof VariableModel){ VariableModel vModel = (VariableModel)m; vModel.removeVariableChangeListener(l); } variableChangeListeners.remove(l); } public synchronized void removeAllVariableChangeListeners(){ TableModel m = variableTable.getModel(); if(m instanceof VariableModel){ VariableModel vModel = (VariableModel)m; vModel.removeAllVariableChangeListeners(); } variableChangeListeners.clear(); } /** * Call this method when ever database or variables are changed from within the Workspace object or * an analysis object. * * @param e */ public synchronized void fireVariableChangeEvent(VariableChangeEvent e){ for(VariableChangeListener l : variableChangeListeners){ l.variableChanged(e); } } //=============================================================================================================== class VariableListener implements VariableChangeListener{ public void variableChanged(VariableChangeEvent e){ VariableChangeType changeType = e.getChangeType(); if(VariableChangeType.VARIABLE_ADDED==changeType || VariableChangeType.VARIABLE_DELETED==changeType || VariableChangeType.VARIABLE_RENAMED==changeType){ DataTableName evtTableName = e.getTableName(); if(evtTableName.equals(currentDataTable)){ //current table is reloaded any time a variable is added or deleted // reloadTable(e.getTableName()); refreshButton.setEnabled(true); refreshButton.setText("Refresh Data View"); } } } } /** * Implementation of TreeSelectionListener interface */ class JmetrikListSelectionListener implements ListSelectionListener{ public void valueChanged(ListSelectionEvent e){ DataTableName tableName = (DataTableName)workspaceList.getSelectedValue(); if(tableName!=null){ openTable(tableName); } } } class DatabaseChangeListener implements PropertyChangeListener{ public void propertyChange(PropertyChangeEvent e) { String propertyName = e.getPropertyName(); if("import".equals(propertyName)){ //open table and set TreePath to selected table DataTableName tName = (DataTableName)e.getNewValue(); tableListModel.addElement(tName); workspaceList.clearSelection(); //Table will be openned when selected // openTable(tName); // try{ // //rapidly importing many data files will cause an ArrayIndexOutOfBoundsException // workspaceList.setSelectedValue(tName, true); // }catch(ArrayIndexOutOfBoundsException ex){ // logger.warn("Table index out of bounds in list of data tables. No worries.", ex); // } }else if("table-added".equals(propertyName)){ DataTableName dataTableName = (DataTableName)e.getNewValue(); tableListModel.addElement(dataTableName); }else if("table-deleted".equals(propertyName)){ DataTableName dataTableName = (DataTableName)e.getNewValue(); tableListModel.removeElement(dataTableName); workspaceList.clearSelection(); }else if("table-updated".equals(propertyName)){ DataTableName dataTableName = (DataTableName)e.getNewValue(); if(currentDataTable!=null && currentDataTable.equals(dataTableName)){ // reloadTable(dataTableName); refreshButton.setEnabled(true); refreshButton.setText("Refresh Data View"); } } } } }