Java tutorial
/* * JStock - Free Stock Market Software * Copyright (C) 2015 Yan Cheng Cheok <yccheok@yahoo.com> * * 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 2 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.yccheok.jstock.gui; import com.nexes.wizard.*; import java.awt.event.*; import java.io.File; import java.text.MessageFormat; import java.util.*; import java.util.concurrent.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.table.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.yccheok.jstock.alert.GoogleMail; import org.yccheok.jstock.analysis.*; import org.yccheok.jstock.analysis.Indicator; import org.yccheok.jstock.engine.*; import org.yccheok.jstock.internationalization.GUIBundle; import org.yccheok.jstock.internationalization.MessagesBundle; /** * * @author yccheok */ public class IndicatorScannerJPanel extends javax.swing.JPanel implements ChangeListener, org.yccheok.jstock.engine.Observer<Indicator, Boolean> { /** Creates new form IndicatorScannerJPanel */ public IndicatorScannerJPanel() { initComponents(); initTableHeaderToolTips(); this.initGUIOptions(); // Reader and writer locks, so that we can have a correct stop operation. java.util.concurrent.locks.ReadWriteLock readWriteLock = new java.util.concurrent.locks.ReentrantReadWriteLock(); reader = readWriteLock.readLock(); writer = readWriteLock.writeLock(); // Get ready with all the data structures when pressing "start". initRealTimeStockMonitor(); initStockHistoryMonitor(); initAlertDataStructures(); initCompleteProgressDataStructures(); } /** This method is called from within the constructor to * initialize the form. * WARNING: Do NOT modify this code. The content of this method is * always regenerated by the Form Editor. */ // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { jPanel1 = new javax.swing.JPanel(); jButton1 = new javax.swing.JButton(); jButton2 = new javax.swing.JButton(); jPanel2 = new javax.swing.JPanel(); jScrollPane1 = new javax.swing.JScrollPane(); jTable1 = new javax.swing.JTable(); setLayout(new java.awt.BorderLayout(5, 5)); jButton1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/player_play.png"))); // NOI18N java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui"); // NOI18N jButton1.setText(bundle.getString("IndicatorScannerJPanel_Scan...")); // NOI18N jButton1.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jButton1ActionPerformed(evt); } }); jPanel1.add(jButton1); jButton2.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/stop.png"))); // NOI18N jButton2.setText(bundle.getString("IndicatorScannerJPanel_Stop")); // NOI18N jButton2.setEnabled(false); jButton2.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jButton2ActionPerformed(evt); } }); jPanel1.add(jButton2); add(jPanel1, java.awt.BorderLayout.SOUTH); jPanel2.setBorder(javax.swing.BorderFactory .createTitledBorder(bundle.getString("IndicatorScannerJPanel_IndicatorScanResult"))); // NOI18N jPanel2.setLayout(new java.awt.BorderLayout()); jTable1.setAutoCreateRowSorter(true); jTable1.setFont(jTable1.getFont().deriveFont(jTable1.getFont().getStyle() | java.awt.Font.BOLD, jTable1.getFont().getSize() + 1)); jTable1.setModel(new IndicatorTableModel()); jTable1.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_OFF); this.jTable1.setDefaultRenderer(Number.class, new StockTableCellRenderer(SwingConstants.RIGHT)); this.jTable1.setDefaultRenderer(Double.class, new StockTableCellRenderer(SwingConstants.RIGHT)); this.jTable1.setDefaultRenderer(Object.class, new StockTableCellRenderer(SwingConstants.LEFT)); this.jTable1.getTableHeader().addMouseListener(new TableColumnSelectionPopupListener(2)); this.jTable1.addMouseListener(new TableRowPopupListener()); this.jTable1.addKeyListener(new TableKeyEventListener()); if (JStock.instance().getJStockOptions().useLargeFont()) { this.jTable1.setRowHeight((int) (this.jTable1.getRowHeight() * Constants.FONT_ENLARGE_FACTOR)); } jScrollPane1.setViewportView(jTable1); jPanel2.add(jScrollPane1, java.awt.BorderLayout.CENTER); add(jPanel2, java.awt.BorderLayout.CENTER); }// </editor-fold>//GEN-END:initComponents private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton2ActionPerformed stop(); }//GEN-LAST:event_jButton2ActionPerformed private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed // this.startScanThread must be null, as "stop" button must be pressed // before we can press "start" button. assert (this.startScanThread == null); JOptionPane.showMessageDialog(this, MessagesBundle.getString("info_message_stop_indicator_scanner_once_done"), MessagesBundle.getString("info_title_stop_indicator_scanner_once_done"), JOptionPane.INFORMATION_MESSAGE); stop_button_pressed = false; final JStock m = JStock.instance(); if (m.getStockInfoDatabase() == null) { javax.swing.JOptionPane.showMessageDialog(this, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages") .getString("info_message_we_havent_connected_to_stock_server"), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages") .getString("info_title_we_havent_connected_to_stock_server"), javax.swing.JOptionPane.INFORMATION_MESSAGE); return; } // Reset dirty flag, to allow background thread to show indicator on // the table. allowIndicatorShown = true; initWizardDialog(); int ret = wizard.showModalDialog(680, -1, false); if (ret != Wizard.FINISH_RETURN_CODE) return; final WizardModel wizardModel = wizard.getModel(); this.startScanThread = getStartScanThread(wizardModel); this.startScanThread.start(); jButton1.setEnabled(false); jButton2.setEnabled(true); m.setStatusBar(true, GUIBundle.getString("IndicatorScannerJPanel_IndicatorScannerIsScanning...")); }//GEN-LAST:event_jButton1ActionPerformed /** * Initialize GUI options of this indicator scanner panel. */ public void initGUIOptions() { File f = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "indicatorscannerjpanel.xml"); GUIOptions guiOptions = org.yccheok.jstock.gui.Utils.fromXML(GUIOptions.class, f); if (guiOptions == null) { // When user launches JStock for first time, we will help him to // turn off the following column(s), as we feel those information // is redundant. If they wish to view those information, they have // to turn it on explicitly. JTableUtilities.removeTableColumn(jTable1, GUIBundle.getString("MainFrame_Open")); return; } if (guiOptions.getJTableOptionsSize() <= 0) { // When user launches JStock for first time, we will help him to // turn off the following column(s), as we feel those information // is redundant. If they wish to view those information, they have // to turn it on explicitly. JTableUtilities.removeTableColumn(jTable1, GUIBundle.getString("MainFrame_Open")); return; } /* Set Table Settings */ JTableUtilities.setJTableOptions(jTable1, guiOptions.getJTableOptions(0)); } public boolean saveGUIOptions() { if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config") == false) { return false; } final GUIOptions.JTableOptions jTableOptions = new GUIOptions.JTableOptions(); final int count = this.jTable1.getColumnCount(); for (int i = 0; i < count; i++) { final String name = this.jTable1.getColumnName(i); final TableColumn column = jTable1.getColumnModel().getColumn(i); jTableOptions .addColumnOption(GUIOptions.JTableOptions.ColumnOption.newInstance(name, column.getWidth())); } final GUIOptions guiOptions = new GUIOptions(); guiOptions.addJTableOptions(jTableOptions); File f = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "indicatorscannerjpanel.xml"); return org.yccheok.jstock.gui.Utils.toXML(guiOptions, f); } // Time consuming method. It involves file I/O reading (getOperatorIndicator). private void initOperatorIndicators(WizardModel wizardModel) { // Clear the previous operator indicators. this.operatorIndicators.clear(); WizardPanelDescriptor wizardPanelDescriptor0 = wizardModel .getPanelDescriptor(WizardSelectStockDescriptor.IDENTIFIER); WizardSelectStockJPanel wizardSelectStockJPanel = (WizardSelectStockJPanel) wizardPanelDescriptor0 .getPanelComponent(); WizardPanelDescriptor wizardPanelDescriptor1 = wizardModel .getPanelDescriptor(WizardSelectIndicatorDescriptor.IDENTIFIER); WizardSelectIndicatorJPanel wizardSelectIndicatorJPanel = (WizardSelectIndicatorJPanel) wizardPanelDescriptor1 .getPanelComponent(); final JStock m = JStock.instance(); final IndicatorProjectManager alertIndicatorProjectManager = m.getAlertIndicatorProjectManager(); java.util.List<String> projects = wizardSelectIndicatorJPanel.getSelectedProjects(); java.util.List<StockInfo> stockInfos = wizardSelectStockJPanel.getSelectedStockInfos(); for (final StockInfo stockInfo : stockInfos) { if (this.stop_button_pressed) { return; } final java.util.List<OperatorIndicator> result = new java.util.ArrayList<OperatorIndicator>(); this.operatorIndicators.put(stockInfo.code, result); for (String project : projects) { final OperatorIndicator operatorIndicator = alertIndicatorProjectManager .getOperatorIndicator(project); if (operatorIndicator != null) { final Stock stock = org.yccheok.jstock.engine.Utils.getEmptyStock(stockInfo); operatorIndicator.setStock(stock); result.add(operatorIndicator); } try { /* Some users with low computer spec, complain that their CPUs usage are high. * My experience is that, 200ms sleep time will be enough to rest their CPUs. * I am not too sure about 50ms. Let's just wait and see... * When user runs this Indicator Scanner, he is expecting that he needs to wait. * So, it doesn't matter that we let him "wait" for extra 50ms seconds every round. * Some more, he shall feel more happy, to see his computer more responsive. */ Thread.sleep(50); } catch (InterruptedException ex) { log.error(null, ex); break; } } /* for(String project : projects) */ this.submitOperatorIndicatorToMonitor(result); } /* for(String code : codes) */ } private void submitOperatorIndicatorToMonitor(java.util.List<OperatorIndicator> indicators) { Duration historyDuration = Duration.getTodayDurationByDays(0); for (OperatorIndicator operatorIndicator : indicators) { historyDuration = historyDuration.getUnionDuration(operatorIndicator.getNeededStockHistoryDuration()); } // Duration must be initialized, before codes being added. this.stockHistoryMonitor.setDuration(historyDuration); boolean done = true; for (OperatorIndicator operatorIndicator : indicators) { /* Hacking way to make startScanThread stop within a very short time. */ if (this.stop_button_pressed) { return; } if (operatorIndicator.isStockHistoryCalculationDone() == false) { done = false; // Early break. We will let history monitor to perform pre-calculation. break; } else { operatorIndicator.preCalculate(); } } if (indicators.size() > 0) { // All indicator in indicators, will be having same code. final Code code = indicators.get(0).getStock().code; if (done) { // Perform real time monitoring, for the code with history information. realTimeStockMonitor.addStockCode(code); realTimeStockMonitor.startNewThreadsIfNecessary(); realTimeStockMonitor.refresh(); } else { // Try to load history from disk first. StockHistoryServer stockHistoryServer = this.stockHistoryMonitor.getStockHistoryServer(code); if (stockHistoryServer == null) { this.stockHistoryMonitor.addStockCode(code); } else { processHistory(code, stockHistoryServer); } } } } @Override public void update(final Indicator indicator, Boolean result) { // Use local variables, to ensure we do not consume the newly // initialized variables after stop(). The code should be placed before // "if (this.stop_button_pressed)" check. ExecutorService _emailAlertPool = null; ExecutorService _systemTrayAlertPool = null; // There are 2 reasons why we are applying lock right here. // 1) Ensure visibility, as we do not apply volatile in all member // variables. // 2) Make sure it is mutual exclusive with stop operation. reader.lock(); try { _emailAlertPool = this.emailAlertPool; _systemTrayAlertPool = this.systemTrayAlertPool; if (this.stop_button_pressed) { return; } } finally { reader.unlock(); } final boolean flag = result; if (flag == false) { removeIndicatorFromTable(indicator); return; } addIndicatorToTable(indicator); final JStock m = JStock.instance(); final JStockOptions jStockOptions = m.getJStockOptions(); if (jStockOptions.isPopupMessage()) { final Runnable r = new Runnable() { @Override public void run() { final Stock stock = indicator.getStock(); final double price = stock.getLastPrice(); final String template = GUIBundle.getString("IndicatorScannerJPanel_Hit_template"); final String message = MessageFormat.format(template, stock.symbol, price, indicator.toString()); if (jStockOptions.isPopupMessage()) { m.displayPopupMessage(stock.symbol.toString(), message); if (jStockOptions.isSoundEnabled()) { /* Non-blocking. */ Utils.playAlertSound(); } try { // Make it rest for 2 minute. Yahoo does have quota // for every ip. If we are too greedy, we will get // Error message: "Unable to process request at this time -- error 999" // https://help.yahoo.com/kb/SLN2253.html Thread.sleep(2 * 60 * 1000); //Thread.sleep(jStockOptions.getAlertSpeed() * 1000); } catch (InterruptedException exp) { log.error(null, exp); } } } }; try { _systemTrayAlertPool.submit(r); } catch (java.util.concurrent.RejectedExecutionException exp) { log.error(null, exp); } } /* if (jStockOptions.isPopupMessage()) */ // Sound alert hasn't been submitted to pop up message pool. if (jStockOptions.isPopupMessage() == false && jStockOptions.isSoundEnabled()) { final Runnable r = new Runnable() { @Override public void run() { if (jStockOptions.isSoundEnabled()) { /* Non-blocking. */ Utils.playAlertSound(); try { Thread.sleep(jStockOptions.getAlertSpeed() * 1000); } catch (InterruptedException exp) { log.error(null, exp); } } } }; try { _systemTrayAlertPool.submit(r); } catch (java.util.concurrent.RejectedExecutionException exp) { log.error(null, exp); } } /* if (this.jStockOptions.isSoundEnabled()) */ if (jStockOptions.isSendEmail()) { final Runnable r = new Runnable() { @Override public void run() { final Stock stock = indicator.getStock(); final double price = stock.getLastPrice(); final String template = GUIBundle.getString("IndicatorScannerJPanel_Hit_template"); final String title = MessageFormat.format(template, stock.symbol, price, indicator.toString()); final String message = title + "\n(JStock)"; final String ccEmail = Utils.decrypt(jStockOptions.getCCEmail()); try { GoogleMail.Send(ccEmail, title, message); } catch (Exception ex) { log.error(null, ex); } } }; try { _emailAlertPool.submit(r); } catch (java.util.concurrent.RejectedExecutionException exp) { log.error(null, exp); } } } private static class ColumnHeaderToolTips extends MouseMotionAdapter { // Current column whose tooltip is being displayed. // This variable is used to minimize the calls to setToolTipText(). TableColumn curCol; // Maps TableColumn objects to tooltips Map<TableColumn, String> tips = new HashMap<TableColumn, String>(); // If tooltip is null, removes any tooltip text. public void setToolTip(TableColumn col, String tooltip) { if (tooltip == null) { tips.remove(col); } else { tips.put(col, tooltip); } } @Override public void mouseMoved(MouseEvent evt) { TableColumn col = null; JTableHeader header = (JTableHeader) evt.getSource(); JTable table = header.getTable(); TableColumnModel colModel = table.getColumnModel(); int vColIndex = colModel.getColumnIndexAtX(evt.getX()); // Return if not clicked on any column header if (vColIndex >= 0) { col = colModel.getColumn(vColIndex); } if (col != curCol) { header.setToolTipText((String) tips.get(col)); curCol = col; } } } private void initTableHeaderToolTips() { JTableHeader header = jTable1.getTableHeader(); ColumnHeaderToolTips tips = new ColumnHeaderToolTips(); header.addMouseMotionListener(tips); } @Override public void stateChanged(javax.swing.event.ChangeEvent evt) { } public void clear() { this.initRealTimeStockMonitor(); this.initStockHistoryMonitor(); this.operatorIndicators.clear(); // Ask help from dirty flag, so that background thread won't have // chance to show indicators on the table. allowIndicatorShown = false; removeAllIndicatorsFromTable(); final JStock m = JStock.instance(); m.setStatusBar(false, GUIBundle.getString("IndicatorScannerJPanel_Connected")); } public void stop() { writer.lock(); try { /* Hacking way to make startScanThread stop within a very short time. */ stop_button_pressed = true; // We must ensure there is no reader locking mechanism within // startScanThread. If not, deadlock might happen. final Thread thread = this.startScanThread; this.startScanThread = null; if (thread != null) { thread.interrupt(); try { thread.join(); } catch (InterruptedException ex) { log.error(null, ex); } } final JStock m = JStock.instance(); this.initRealTimeStockMonitor(); this.initStockHistoryMonitor(); this.initAlertDataStructures(); this.initCompleteProgressDataStructures(); } finally { writer.unlock(); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { jButton1.setEnabled(true); jButton2.setEnabled(false); } }); JStock.instance().setStatusBar(false, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("IndicatorScannerJPanel_Connected")); } private void initWizardDialog() { final JStock m = JStock.instance(); wizard = new Wizard(m); wizard.getDialog().setTitle(java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("IndicatorScannerJPanel_IndicatorScanningWizard")); wizard.getDialog().setResizable(false); WizardPanelDescriptor wizardSelectIndicatorDescriptor = new WizardSelectIndicatorDescriptor(); wizard.registerWizardPanel(WizardSelectIndicatorDescriptor.IDENTIFIER, wizardSelectIndicatorDescriptor); // Quick hack. WizardSelectStockJPanel has no way to obtain MainFrame, during its construction // stage. WizardPanelDescriptor wizardSelectStockDescriptor = new WizardSelectStockDescriptor( m.getStockInfoDatabase()); wizard.registerWizardPanel(WizardSelectStockDescriptor.IDENTIFIER, wizardSelectStockDescriptor); wizard.setCurrentPanel(WizardSelectIndicatorDescriptor.IDENTIFIER); // Center to screen. wizard.getDialog().setLocationRelativeTo(null); } public void updateScanningSpeed(int speed) { this.realTimeStockMonitor.setDelay(speed); } public final void initStockHistoryMonitor() { final StockHistoryMonitor oldStockHistoryMonitor = stockHistoryMonitor; if (oldStockHistoryMonitor != null) { Utils.getZoombiePool().execute(new Runnable() { @Override public void run() { log.info("Prepare to shut down " + oldStockHistoryMonitor + "..."); oldStockHistoryMonitor.clearStockCodes(); oldStockHistoryMonitor.dettachAll(); oldStockHistoryMonitor.stop(); log.info("Shut down " + oldStockHistoryMonitor + " peacefully."); } }); } this.stockHistoryMonitor = new StockHistoryMonitor(HISTORY_MONITOR_MAX_THREAD); stockHistoryMonitor.attach(stockHistoryMonitorObserver); stockHistoryMonitor.setStockHistorySerializer(new StockHistorySerializer(Utils.getHistoryDirectory())); } private void processHistory(Code code, StockHistoryServer stockHistoryServer) { // Use local variables, to ensure we do not consume the newly // initialized variables after stop(). The code should be placed before // "if (this.stop_button_pressed)" check. Set<Code> _failedCodes = null; RealTimeStockMonitor _realTimeStockMonitor = null; // There are 2 reasons why we are applying lock right here. // 1) Ensure visibility, as we do not apply volatile in all member // variables. // 2) Make sure it is mutual exclusive with stop operation. reader.lock(); try { _failedCodes = this.failedCodes; _realTimeStockMonitor = this.realTimeStockMonitor; if (this.stop_button_pressed) { return; } } finally { reader.unlock(); } final JStock m = JStock.instance(); List<OperatorIndicator> indicators = this.operatorIndicators.get(code); if (indicators == null) { return; } if (stockHistoryServer == null) { _failedCodes.add(code); // Probably the network is down. Do not ever retry infinityly. Go // green. :) //monitor.addStockCode(code); return; } _failedCodes.remove(code); Symbol symbol = m.getStockInfoDatabase().codeToSymbol(code); final String template = GUIBundle.getString("IndicatorScannerJPanel_IndicatorScannerFoundHistory_template"); final String message = MessageFormat.format(template, symbol != null ? symbol : code, getCompleteScannedStocksPercentage()); this.updateStatusBarIfStopButtonIsNotPressed(message); for (OperatorIndicator operatorIndicator : indicators) { if (operatorIndicator.isStockHistoryServerNeeded()) { operatorIndicator.setStockHistoryServer(stockHistoryServer); } operatorIndicator.preCalculate(); } // Perform real time monitoring, for the code with history information. _realTimeStockMonitor.addStockCode(code); _realTimeStockMonitor.startNewThreadsIfNecessary(); _realTimeStockMonitor.refresh(); } private void update(StockHistoryMonitor monitor, StockHistoryMonitor.StockHistoryRunnable runnable) { final Code code = runnable.getCode(); final StockHistoryServer stockHistoryServer = runnable.getStockHistoryServer(); processHistory(code, stockHistoryServer); } private org.yccheok.jstock.engine.Observer<StockHistoryMonitor, StockHistoryMonitor.StockHistoryRunnable> getStockHistoryMonitorObserver() { return new org.yccheok.jstock.engine.Observer<StockHistoryMonitor, StockHistoryMonitor.StockHistoryRunnable>() { @Override public void update(StockHistoryMonitor monitor, StockHistoryMonitor.StockHistoryRunnable runnable) { IndicatorScannerJPanel.this.update(monitor, runnable); } }; } // Initializes data structure used for complete progress calculation. private void initCompleteProgressDataStructures() { Set<Code> oldSuccessCodes = successCodes; Set<Code> oldFailedCodes = failedCodes; if (oldSuccessCodes != null) { oldSuccessCodes.clear(); } if (oldFailedCodes != null) { oldFailedCodes.clear(); } successCodes = new java.util.concurrent.CopyOnWriteArraySet<Code>(); failedCodes = new java.util.concurrent.CopyOnWriteArraySet<Code>(); } // Initializes data structure used for alerting purpose. private void initAlertDataStructures() { AlertStateManager oldAlertStateManager = alertStateManager; if (oldAlertStateManager != null) { oldAlertStateManager.dettachAll(); oldAlertStateManager.clearState(); } final ExecutorService oldSystemTrayAlertPool = systemTrayAlertPool; final ExecutorService oldEmailAlertPool = emailAlertPool; Utils.getZoombiePool().execute(new Runnable() { @Override public void run() { if (oldSystemTrayAlertPool != null) { log.info("Prepare to shut down " + oldSystemTrayAlertPool + "..."); oldSystemTrayAlertPool.shutdownNow(); try { oldSystemTrayAlertPool.awaitTermination(100, TimeUnit.DAYS); } catch (InterruptedException exp) { log.error(null, exp); } log.info("Shut down " + oldSystemTrayAlertPool + " peacefully."); log.info("Prepare to shut down " + oldEmailAlertPool + "..."); } if (oldEmailAlertPool != null) { oldEmailAlertPool.shutdownNow(); try { oldEmailAlertPool.awaitTermination(100, TimeUnit.DAYS); } catch (InterruptedException exp) { log.error(null, exp); } log.info("Shut down " + oldEmailAlertPool + " peacefully."); } } }); alertStateManager = new AlertStateManager(); alertStateManager.attach(this); emailAlertPool = Executors.newFixedThreadPool(1); systemTrayAlertPool = Executors.newFixedThreadPool(1); } public final void initRealTimeStockMonitor() { final RealTimeStockMonitor oldRealTimeStockMonitor = this.realTimeStockMonitor; if (oldRealTimeStockMonitor != null) { Utils.getZoombiePool().execute(new Runnable() { @Override public void run() { log.info("Prepare to shut down " + oldRealTimeStockMonitor + "..."); oldRealTimeStockMonitor.clearStockCodes(); oldRealTimeStockMonitor.dettachAll(); oldRealTimeStockMonitor.stop(); log.info("Shut down " + oldRealTimeStockMonitor + " peacefully."); } }); } this.realTimeStockMonitor = new RealTimeStockMonitor(Constants.REAL_TIME_STOCK_MONITOR_MAX_THREAD, Constants.REAL_TIME_STOCK_MONITOR_MAX_STOCK_SIZE_PER_SCAN, JStock.instance().getJStockOptions().getScanningSpeed()); this.realTimeStockMonitor.attach(this.realTimeStockMonitorObserver); } // This is the workaround to overcome Erasure by generics. We are unable to make MainFrame to // two observers at the same time. private org.yccheok.jstock.engine.Observer<RealTimeStockMonitor, java.util.List<Stock>> getRealTimeStockMonitorObserver() { return new org.yccheok.jstock.engine.Observer<RealTimeStockMonitor, java.util.List<Stock>>() { @Override public void update(RealTimeStockMonitor monitor, java.util.List<Stock> stocks) { IndicatorScannerJPanel.this.update(monitor, stocks); } }; } private void updateStatusBarIfStopButtonIsNotPressed(String message) { // Do we need to apply lock right here? if (this.stop_button_pressed) { return; } JStock.instance().setStatusBar(true, message); } private void update(RealTimeStockMonitor monitor, final java.util.List<Stock> stocks) { // Use local variables, to ensure we do not consume the newly // initialized variables after stop(). The code should be placed before // "if (this.stop_button_pressed)" check. AlertStateManager _alertStateManager = null; Set<Code> _successCodes = null; // There are 2 reasons why we are applying lock right here. // 1) Ensure visibility, as we do not apply volatile in all member // variables. // 2) Make sure it is mutual exclusive with stop operation. reader.lock(); try { _alertStateManager = this.alertStateManager; _successCodes = this.successCodes; RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; // Perform "_realTimeStockMonitor != monitor" check, to ensure we // are not using newly constructed realTimeStockMonitor. By just // merely using stop_button_pressed guard flag will not work as, // // 1) User presses on stop button. // 2) Old realTimeStockMonitor may stall. // 3) User presses on start button, and create new realTimeStockMonitor. // 4) stop_button_pressed has became true. // 5) Old realTimeStockMonitor resume. This may cause both old and // new realTimeStockMonitor running. if (this.stop_button_pressed || _realTimeStockMonitor != monitor) { return; } } finally { reader.unlock(); } final boolean isSymbolImmutable = org.yccheok.jstock.engine.Utils.isSymbolImmutable(); for (int i = 0, size = stocks.size(); i < size; i++) { final Stock stock = stocks.get(i); Stock new_stock = stock; // Special handling for China stock market. Also, sometimes for // other countries, Yahoo will return empty string for their symbol. // We will fix it through offline database. if (isSymbolImmutable || new_stock.symbol.toString().isEmpty()) { // Use local variable to ensure thread safety. final StockInfoDatabase stock_info_database = JStock.instance().getStockInfoDatabase(); if (stock_info_database != null) { final Symbol symbol = stock_info_database.codeToSymbol(stock.code); if (symbol != null) { new_stock = new_stock.deriveStock(symbol); } else { // Shouldn't be null. Let's give some warning on this. log.error("Wrong stock code " + stock.code + " given by stock server."); } if (stock != new_stock) { stocks.set(i, new_stock); } } // if (symbol_database != null) } // if (org.yccheok.jstock.engine.Utils.isSymbolImmutable() || new_stock.symbol.toString().isEmpty()) } // for (int i = 0, size = stocks.size(); i < size; i++) if (stocks.size() > 0) { // We only print out the first stock, to avoid too many different // messages within a short duration. final String template = GUIBundle .getString("IndicatorScannerJPanel_IndicatorScannerIsScanning..._template"); final String message = MessageFormat.format(template, stocks.get(0).symbol, getCompleteScannedStocksPercentage()); updateStatusBarIfStopButtonIsNotPressed(message); } for (Stock stock : stocks) { final java.util.List<OperatorIndicator> indicators = this.operatorIndicators.get(stock.code); if (indicators == null) { continue; } final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); if (jStockOptions.isSingleIndicatorAlert()) { for (OperatorIndicator indicator : indicators) { indicator.setStock(stock); _alertStateManager.alert(indicator); } } else { // Multiple indicators alert. for (OperatorIndicator indicator : indicators) { indicator.setStock(stock); } _alertStateManager.alert(indicators); } // Indicates we has finished scanning this stock. _successCodes.add(stock.code); } // Display the same message again, so that we will get the most updated // complete percentage. Should we use back the same message template? if (stocks.size() > 0) { final String template = GUIBundle .getString("IndicatorScannerJPanel_IndicatorScannerIsScanning..._template"); final String message = MessageFormat.format(template, stocks.get(0).symbol, getCompleteScannedStocksPercentage()); updateStatusBarIfStopButtonIsNotPressed(message); } } private int getCompleteScannedStocksPercentage() { int expected = operatorIndicators.size(); int failedCodesSize = failedCodes.size(); int successCodesSize = successCodes.size(); // As long as there is a least 1 success stock, we will consider failed // stocks as well. This is a very crude way, to determine Internet // connection is available, and the failed stocks are just caused by // incorrect stock codes. if ((successCodesSize > 0) && (expected > 0)) { return (successCodesSize + failedCodesSize) * 100 / expected; } return 0; } // Should we synchronized the jTable1, or post the job at GUI event dispatch // queue? private void addIndicatorToTable(final Indicator indicator) { final Runnable r = new Runnable() { @Override public void run() { IndicatorTableModel tableModel = (IndicatorTableModel) jTable1.getModel(); // Dirty way to prevent background thread from showing indicators // on the table. if (allowIndicatorShown) { tableModel.addIndicator(indicator); } } }; SwingUtilities.invokeLater(r); } private void removeIndicatorFromTable(final Indicator indicator) { final Runnable r = new Runnable() { @Override public void run() { IndicatorTableModel tableModel = (IndicatorTableModel) jTable1.getModel(); tableModel.removeIndicator(indicator); } }; SwingUtilities.invokeLater(r); } private void removeAllIndicatorsFromTable() { final Runnable r = new Runnable() { @Override public void run() { IndicatorTableModel tableModel = (IndicatorTableModel) jTable1.getModel(); tableModel.removeAll(); } }; SwingUtilities.invokeLater(r); } private class TableRowPopupListener extends MouseAdapter { @Override public void mouseClicked(MouseEvent evt) { } @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { if (jTable1.getSelectedRowCount() > 0) { getMyJTablePopupMenu().show(e.getComponent(), e.getX(), e.getY()); } } } } private ImageIcon getImageIcon(String imageIcon) { return new javax.swing.ImageIcon(getClass().getResource(imageIcon)); } private JPopupMenu getMyJTablePopupMenu() { JPopupMenu popup = new JPopupMenu(); final JStock m = JStock.instance(); javax.swing.JMenuItem menuItem = new JMenuItem(java.util.ResourceBundle .getBundle("org/yccheok/jstock/data/gui").getString("IndicatorScannerJPanel_History..."), this.getImageIcon("/images/16x16/strokedocker.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { int rows[] = jTable1.getSelectedRows(); final IndicatorTableModel tableModel = (IndicatorTableModel) jTable1.getModel(); for (int row : rows) { final int modelIndex = jTable1.convertRowIndexToModel(row); final Indicator indicator = tableModel.getIndicator(modelIndex); if (indicator != null) { m.displayHistoryChart(indicator.getStock()); } } } }); popup.add(menuItem); menuItem = new JMenuItem(java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("IndicatorScannerJPanel_News..."), this.getImageIcon("/images/16x16/news.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { int rows[] = jTable1.getSelectedRows(); final IndicatorTableModel tableModel = (IndicatorTableModel) jTable1.getModel(); for (int row : rows) { final int modelIndex = jTable1.convertRowIndexToModel(row); final Indicator indicator = tableModel.getIndicator(modelIndex); if (indicator != null) { m.displayStockNews(indicator.getStock()); } } } }); popup.add(menuItem); popup.addSeparator(); menuItem = new JMenuItem(GUIBundle.getString("IndicatorScannerJPanel_AddToRealTimeInfo"), this.getImageIcon("/images/16x16/add.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { int rows[] = jTable1.getSelectedRows(); final IndicatorTableModel tableModel = (IndicatorTableModel) jTable1.getModel(); for (int row : rows) { final int modelIndex = jTable1.convertRowIndexToModel(row); final Indicator indicator = tableModel.getIndicator(modelIndex); if (indicator != null) { m.addStockToTable(indicator.getStock()); } } } }); popup.add(menuItem); if (jTable1.getSelectedRowCount() == 1) { popup.addSeparator(); menuItem = new JMenuItem(GUIBundle.getString("IndicatorScannerJPanel_Buy..."), this.getImageIcon("/images/16x16/inbox.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { final int row = jTable1.getSelectedRow(); final int modelIndex = jTable1.getRowSorter().convertRowIndexToModel(row); final IndicatorTableModel tableModel = (IndicatorTableModel) jTable1.getModel(); final Indicator indicator = tableModel.getIndicator(modelIndex); final Stock stock = indicator.getStock(); JStock.instance().getPortfolioManagementJPanel().showNewBuyTransactionJDialog(stock, stock.getLastPrice(), false); } }); popup.add(menuItem); } return popup; } public void repaintTable() { jTable1.repaint(); } public void clearTableSelection() { jTable1.getSelectionModel().clearSelection(); } public boolean saveAsCSVFile(File file) { final TableModel tableModel = jTable1.getModel(); // Unexpected result may happen while scanning is running, as table // model will be mutated during the middle of writting. Currently, we // do not have solution. final org.yccheok.jstock.file.Statements statements = org.yccheok.jstock.file.Statements .newInstanceFromTableModel(tableModel, false); assert (statements != null); return statements.saveAsCSVFile(file); } public boolean saveAsExcelFile(File file) { final TableModel tableModel = jTable1.getModel(); // Unexpected result may happen while scanning is running, as table // model will be mutated during the middle of writting. Currently, we // do not have solution. final org.yccheok.jstock.file.Statements statements = org.yccheok.jstock.file.Statements .newInstanceFromTableModel(tableModel, false); assert (statements != null); return statements.saveAsExcelFile(file, GUIBundle.getString("IndicatorScannerJPanel_Title")); } private Thread getStartScanThread(final WizardModel wizardModel) { return new Thread(new Runnable() { @Override public void run() { WizardPanelDescriptor wizardPanelDescriptor0 = wizardModel .getPanelDescriptor(WizardSelectStockDescriptor.IDENTIFIER); WizardSelectStockJPanel wizardSelectStockJPanel = (WizardSelectStockJPanel) wizardPanelDescriptor0 .getPanelComponent(); if (wizardSelectStockJPanel.buildSelectedStockCodes() == false) { // Unlikely. log.error("Fail to build selected stock"); return; } removeAllIndicatorsFromTable(); initOperatorIndicators(wizardModel); } }); } private class TableKeyEventListener extends java.awt.event.KeyAdapter { @Override public void keyTyped(java.awt.event.KeyEvent e) { IndicatorScannerJPanel.this.clearTableSelection(); } } public void refreshRealTimeStockMonitor() { RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; if (_realTimeStockMonitor != null) { _realTimeStockMonitor.refresh(); } } public void rebuildRealTimeStockMonitor() { RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; if (_realTimeStockMonitor != null) { _realTimeStockMonitor.rebuild(); } } private Wizard wizard; private RealTimeStockMonitor realTimeStockMonitor; private final org.yccheok.jstock.engine.Observer<RealTimeStockMonitor, java.util.List<Stock>> realTimeStockMonitorObserver = this .getRealTimeStockMonitorObserver(); private final java.util.Map<Code, java.util.List<OperatorIndicator>> operatorIndicators = new java.util.concurrent.ConcurrentHashMap<Code, java.util.List<OperatorIndicator>>(); private Set<Code> successCodes; private Set<Code> failedCodes; private AlertStateManager alertStateManager; private ExecutorService emailAlertPool; private ExecutorService systemTrayAlertPool; private final org.yccheok.jstock.engine.Observer<StockHistoryMonitor, StockHistoryMonitor.StockHistoryRunnable> stockHistoryMonitorObserver = this .getStockHistoryMonitorObserver(); private StockHistoryMonitor stockHistoryMonitor = null; // Dirty flag to be used with clear method and start button method. // Ensure we have an instant way to prevent background thread from showing // indicators on the table, after we call clear method. // This is a dirty way, but it just work :) private volatile Boolean allowIndicatorShown = true; // This boolean flag is important, as we are unable to use // this.startScanThread != currentThread to stop an operation. We need to // stop several threads at the same time such as RealTimeStockMonitor. private volatile boolean stop_button_pressed = true; // Reader and writer locks, so that we can have a correct stop operation. private final java.util.concurrent.locks.Lock reader; private final java.util.concurrent.locks.Lock writer; // There isn't any need to make this thread volatile, as we do not use // this.startScanThread != currentThread technique to stop a thread. This is // just to be consistence across entire project. private volatile Thread startScanThread = null; private static final Log log = LogFactory.getLog(IndicatorScannerJPanel.class); private static final int HISTORY_MONITOR_MAX_THREAD = 4; // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton jButton1; private javax.swing.JButton jButton2; private javax.swing.JPanel jPanel1; private javax.swing.JPanel jPanel2; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JTable jTable1; // End of variables declaration//GEN-END:variables }