org.yccheok.jstock.gui.IndicatorScannerJPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.yccheok.jstock.gui.IndicatorScannerJPanel.java

Source

/*
 * 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

}