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 org.yccheok.jstock.engine.Pair; import com.google.api.client.auth.oauth2.Credential; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.awt.*; import java.awt.event.*; import java.io.*; import java.io.IOException; import java.text.MessageFormat; import java.util.*; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javafx.application.Platform; import javax.swing.*; 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.Indicator; import org.yccheok.jstock.analysis.OperatorIndicator; import org.yccheok.jstock.engine.*; import org.yccheok.jstock.engine.Industry; import org.yccheok.jstock.file.Atom; import org.yccheok.jstock.file.GUIBundleWrapper; import org.yccheok.jstock.file.GUIBundleWrapper.Language; import org.yccheok.jstock.file.Statement; import org.yccheok.jstock.file.Statements; import org.yccheok.jstock.gui.charting.ChartJDialog; import org.yccheok.jstock.gui.charting.ChartJDialogOptions; import org.yccheok.jstock.gui.charting.DynamicChart; import org.yccheok.jstock.gui.portfolio.PortfolioJDialog; import org.yccheok.jstock.gui.table.NonNegativeDoubleEditor; import org.yccheok.jstock.gui.watchlist.WatchlistJDialog; import org.yccheok.jstock.internationalization.GUIBundle; import org.yccheok.jstock.internationalization.MessagesBundle; import org.yccheok.jstock.network.ProxyDetector; import org.yccheok.jstock.portfolio.PortfolioInfo; import org.yccheok.jstock.watchlist.WatchlistInfo; import org.yccheok.jstock.engine.Country; import org.yccheok.jstock.engine.Stock; import org.yccheok.jstock.engine.StockInfo; import org.yccheok.jstock.file.ThreadSafeFileLock; import org.yccheok.jstock.gui.news.StockNewsJFrame; /** * * @author doraemon */ public class JStock extends javax.swing.JFrame { public static final class CSVWatchlist { public final TableModel tableModel; private CSVWatchlist(TableModel tableModel) { if (tableModel == null) { throw new java.lang.IllegalArgumentException(); } this.tableModel = tableModel; } public static CSVWatchlist newInstance(TableModel tableModel) { return new CSVWatchlist(tableModel); } } // Comment out, to avoid annoying log messages during debugging. static { System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.NoOpLog"); } /** Creates new form MainFrame */ // Private constructor is sufficient to suppress unauthorized calls to the constructor private JStock() { } /** * Initialize this MainFrame based on the JStockOptions. */ private void init() { initComponents(); createLookAndFeelMenuItems(); createCountryMenuItems(); createStockIndicatorEditor(); createIndicatorScannerJPanel(); createPortfolioManagementJPanel(); createIconsAndToolTipTextForJTabbedPane(); this.createSystemTrayIcon(); this.initPreloadDatabase(false); this.initExtraDatas(); this.initStatusBar(); this.initMarketJPanel(); this.initTableHeaderToolTips(); this.initMyJXStatusBarExchangeRateLabelMouseAdapter(); this.initMyJXStatusBarCountryLabelMouseAdapter(); this.initMyJXStatusBarImageLabelMouseAdapter(); this.initStockInfoDatabaseMeta(); this.initDatabase(true); this.initAjaxProvider(); this.initRealTimeIndexMonitor(); this.initLatestNewsTask(); this.initExchangeRateMonitor(); this.initRealTimeStockMonitor(); this.initWatchlist(); this.initAlertStateManager(); this.initDynamicCharts(); this.initDynamicChartVisibility(); this.initAlwaysOnTop(); this.initStockHistoryMonitor(); this.initOthersStockHistoryMonitor(); this.initGUIOptions(); this.initChartJDialogOptions(); this.initLanguageMenuItemsSelection(); this.initJXLayerOnJComboBox(); this.initKeyBindings(); // Turn to the last viewed page. final int lastSelectedPageIndex = this.getJStockOptions().getLastSelectedPageIndex(); if (this.jTabbedPane1.getTabCount() > lastSelectedPageIndex) { this.jTabbedPane1.setSelectedIndex(lastSelectedPageIndex); } // setSelectedIndex will not always trigger jTabbedPane1StateChanged, // if the selected index is same as current page index. However, we are // expecting jTabbedPane1StateChanged to suspend/resume // PortfolioManagementJPanel's RealtTimeStockMonitor and MainFrame's // CurrencyExchangeMonitor, in order to preserve network resource. Hence, // we need to call handleJTabbedPaneStateChanged explicitly. handleJTabbedPaneStateChanged(this.jTabbedPane1); // Restore previous size and location. JStockOptions.BoundsEx boundsEx = jStockOptions.getBoundsEx(); if (boundsEx == null) { // First time. Maximize it. this.setExtendedState(Frame.MAXIMIZED_BOTH); } else { if ((boundsEx.extendedState & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH) { this.setExtendedState(Frame.MAXIMIZED_BOTH); } else { this.setBounds(boundsEx.bounds); } } installShutdownHook(); // Spain no longer supported. Sad... if (this.jStockOptions.getCountry() == Country.Spain) { JOptionPane.showMessageDialog(null, MessagesBundle.getString("info_message_spain_not_supported"), MessagesBundle.getString("info_title_spain_not_supported"), JOptionPane.INFORMATION_MESSAGE); } } private void requestFocusOnJComboBox() { this.jComboBox1.getEditor().getEditorComponent().requestFocus(); } private void initKeyBindings() { KeyStroke watchlistNavigationKeyStroke = KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_W, java.awt.event.InputEvent.CTRL_MASK); KeyStroke portfolioNavigationKeyStroke = KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_P, java.awt.event.InputEvent.CTRL_MASK); getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(watchlistNavigationKeyStroke, "watchlistNavigation"); getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(portfolioNavigationKeyStroke, "portfolioNavigation"); getRootPane().getActionMap().put("watchlistNavigation", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { watchlistNavigation(); } }); getRootPane().getActionMap().put("portfolioNavigation", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { portfolioNavigation(); } }); } private void watchlistNavigation() { if (this.getSelectedComponent() != this.jPanel8) { // The page is not active. Make it active. JStock.this.jTabbedPane1.setSelectedIndex(0); return; } final java.util.List<String> watchlistNames = org.yccheok.jstock.watchlist.Utils.getWatchlistNames(); final int size = watchlistNames.size(); if (size <= 1) { // Nothing to navigate. return; } final String currentWatchlistName = this.getJStockOptions().getWatchlistName(); int index = 0; for (; index < size; index++) { if (watchlistNames.get(index).equals(currentWatchlistName)) { index++; if (index >= size) index = 0; break; } } this.selectActiveWatchlist(watchlistNames.get(index)); } private void portfolioNavigation() { if (this.getSelectedComponent() != this.portfolioManagementJPanel) { // The page is not active. Make it active. JStock.this.jTabbedPane1.setSelectedIndex(3); return; } final java.util.List<String> portfolioNames = org.yccheok.jstock.portfolio.Utils.getPortfolioNames(); final int size = portfolioNames.size(); if (size <= 1) { // Nothing to navigate. return; } final String currentPortfolioName = this.getJStockOptions().getPortfolioName(); int index = 0; for (; index < size; index++) { if (portfolioNames.get(index).equals(currentPortfolioName)) { index++; if (index >= size) index = 0; break; } } this.selectActivePortfolio(portfolioNames.get(index)); } // Register a hook to save app settings when quit via the app menu. // This is in Mac OSX only. // http://sourceforge.net/tracker/?func=detail&aid=3490453&group_id=202896&atid=983418 private void installShutdownHook() { if (Utils.isMacOSX()) { // Triggered by command + Q Runnable runner = new Runnable() { @Override public void run() { if (isFormWindowClosedCalled) { AppLock.unlock(); return; } // 1) Do not call formWindowClosed directly, as accessing UI // will cause "hang". // 2) Calling system.exit will cause "hang" too. JStock.this.save(); if (JStock.this.needToSaveUserDefinedDatabase) { // We are having updated user database in memory. // Save it to disk. JStock.this.saveUserDefinedDatabaseAsCSV(jStockOptions.getCountry(), stockInfoDatabase); } AppLock.unlock(); } }; Runtime.getRuntime().addShutdownHook(new Thread(runner, "Window Prefs Hook")); } else { Runnable runner = new Runnable() { @Override public void run() { AppLock.unlock(); } }; Runtime.getRuntime().addShutdownHook(new Thread(runner, "Window Prefs Hook")); } } /** * Initialize language menu items so that correct item is being selected * according to current default locale. */ private void initLanguageMenuItemsSelection() { // Please revise Statement's construct code, when adding in new language. // So that its language guessing algorithm will work as it is. final Locale defaultLocale = Locale.getDefault(); if (Utils.isTraditionalChinese(defaultLocale)) { this.jRadioButtonMenuItem4.setSelected(true); } else if (Utils.isSimplifiedChinese(defaultLocale)) { this.jRadioButtonMenuItem2.setSelected(true); } else if (defaultLocale.getLanguage().equals(Locale.GERMAN.getLanguage())) { this.jRadioButtonMenuItem3.setSelected(true); } else if (defaultLocale.getLanguage().equals(Locale.ITALIAN.getLanguage())) { this.jRadioButtonMenuItem5.setSelected(true); } else if (defaultLocale.getLanguage().equals(Locale.FRENCH.getLanguage())) { this.jRadioButtonMenuItem6.setSelected(true); } else { this.jRadioButtonMenuItem1.setSelected(true); } } /** * MainFrameHolder is loaded on the first execution of Singleton.getInstance() * or the first access to MainFrameHolder.INSTANCE, not before. */ private static class MainFrameHolder { private final static JStock INSTANCE = new JStock(); } /** * Returns MainFrame as singleton. * * @return MainFrame as singleton */ public static JStock instance() { return MainFrameHolder.INSTANCE; } // Install JXLayer around JComboBox. // It is used to display busy indicator. private void initJXLayerOnJComboBox() { // Add the layer as usual combo box. jPanel1.add(Utils.getBusyJXLayer((AutoCompleteJComboBox) this.jComboBox1)); } /** 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() { buttonGroup1 = new javax.swing.ButtonGroup(); buttonGroup2 = new javax.swing.ButtonGroup(); buttonGroup3 = new javax.swing.ButtonGroup(); buttonGroup4 = new javax.swing.ButtonGroup(); jComboBox1 = new AutoCompleteJComboBox(); jPanel6 = new javax.swing.JPanel(); jTabbedPane1 = new javax.swing.JTabbedPane(); jPanel8 = new javax.swing.JPanel(); jScrollPane1 = new javax.swing.JScrollPane(); jTable1 = new javax.swing.JTable(); jPanel1 = new javax.swing.JPanel(); jLabel1 = new javax.swing.JLabel(); jPanel10 = new javax.swing.JPanel(); jPanel3 = new javax.swing.JPanel(); jPanel2 = new javax.swing.JPanel(); jMenuBar2 = new javax.swing.JMenuBar(); jMenu3 = new javax.swing.JMenu(); jMenuItem2 = new javax.swing.JMenuItem(); jMenuItem9 = new javax.swing.JMenuItem(); jSeparator7 = new javax.swing.JPopupMenu.Separator(); jMenuItem11 = new javax.swing.JMenuItem(); jMenuItem10 = new javax.swing.JMenuItem(); jSeparator8 = new javax.swing.JPopupMenu.Separator(); jMenuItem1 = new javax.swing.JMenuItem(); jMenu5 = new javax.swing.JMenu(); jMenuItem4 = new javax.swing.JMenuItem(); jMenuItem7 = new javax.swing.JMenuItem(); jSeparator4 = new javax.swing.JPopupMenu.Separator(); jMenuItem15 = new javax.swing.JMenuItem(); jMenu6 = new javax.swing.JMenu(); jMenu10 = new javax.swing.JMenu(); jRadioButtonMenuItem1 = new javax.swing.JRadioButtonMenuItem(); jRadioButtonMenuItem2 = new javax.swing.JRadioButtonMenuItem(); jRadioButtonMenuItem4 = new javax.swing.JRadioButtonMenuItem(); jRadioButtonMenuItem6 = new javax.swing.JRadioButtonMenuItem(); jRadioButtonMenuItem3 = new javax.swing.JRadioButtonMenuItem(); jRadioButtonMenuItem5 = new javax.swing.JRadioButtonMenuItem(); jMenu7 = new javax.swing.JMenu(); jMenuItem8 = new javax.swing.JMenuItem(); jMenu9 = new javax.swing.JMenu(); jMenu8 = new javax.swing.JMenu(); jMenu1 = new javax.swing.JMenu(); jMenuItem6 = new javax.swing.JMenuItem(); jMenu4 = new javax.swing.JMenu(); jMenu2 = new javax.swing.JMenu(); jMenuItem3 = new javax.swing.JMenuItem(); jMenuItem16 = new javax.swing.JMenuItem(); jMenuItem12 = new javax.swing.JMenuItem(); jSeparator6 = new javax.swing.JPopupMenu.Separator(); jMenuItem13 = new javax.swing.JMenuItem(); jMenuItem14 = new javax.swing.JMenuItem(); jSeparator5 = new javax.swing.JPopupMenu.Separator(); jMenuItem5 = new javax.swing.JMenuItem(); jMenu11 = new javax.swing.JMenu(); jMenuItem17 = new javax.swing.JMenuItem(); jComboBox1.setEditable(true); jComboBox1.setPreferredSize(new java.awt.Dimension(150, 24)); ((AutoCompleteJComboBox) this.jComboBox1).attachStockInfoObserver(getStockInfoObserver()); ((AutoCompleteJComboBox) this.jComboBox1).attachDispObserver(getDispObserver()); setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE); java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui"); // NOI18N setTitle(bundle.getString("MainFrame_Application_Title")); // NOI18N setIconImage(getMyIconImage()); addMouseListener(new java.awt.event.MouseAdapter() { public void mouseClicked(java.awt.event.MouseEvent evt) { formMouseClicked(evt); } }); addWindowListener(new java.awt.event.WindowAdapter() { public void windowClosed(java.awt.event.WindowEvent evt) { formWindowClosed(evt); } public void windowClosing(java.awt.event.WindowEvent evt) { formWindowClosing(evt); } public void windowDeiconified(java.awt.event.WindowEvent evt) { formWindowDeiconified(evt); } public void windowIconified(java.awt.event.WindowEvent evt) { formWindowIconified(evt); } }); getContentPane().setLayout(new java.awt.BorderLayout(5, 5)); jPanel6.setBorder(javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5)); jPanel6.setLayout(new java.awt.BorderLayout(5, 5)); this.jPanel6.add(statusBar, java.awt.BorderLayout.SOUTH); getContentPane().add(jPanel6, java.awt.BorderLayout.SOUTH); jTabbedPane1.setBorder(javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5)); jTabbedPane1.addChangeListener(new javax.swing.event.ChangeListener() { public void stateChanged(javax.swing.event.ChangeEvent evt) { jTabbedPane1StateChanged(evt); } }); jPanel8.setLayout(new java.awt.BorderLayout(5, 5)); jTable1.setAutoCreateRowSorter(true); jTable1.setFont(jTable1.getFont().deriveFont(jTable1.getFont().getStyle() | java.awt.Font.BOLD, jTable1.getFont().getSize() + 1)); jTable1.setModel(new StockTableModel()); 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.setDefaultEditor(Double.class, new NonNegativeDoubleEditor()); this.jTable1.getModel().addTableModelListener(this.getTableModelListener()); this.jTable1.getTableHeader().addMouseListener(new TableColumnSelectionPopupListener(1)); this.jTable1.addMouseListener(new TableMouseAdapter()); this.jTable1.addKeyListener(new TableKeyEventListener()); if (jStockOptions.useLargeFont()) { this.jTable1.setRowHeight((int) (this.jTable1.getRowHeight() * Constants.FONT_ENLARGE_FACTOR)); } jTable1.addKeyListener(new java.awt.event.KeyAdapter() { public void keyPressed(java.awt.event.KeyEvent evt) { jTable1KeyPressed(evt); } }); jScrollPane1.setViewportView(jTable1); jPanel8.add(jScrollPane1, java.awt.BorderLayout.CENTER); jLabel1.setText(bundle.getString("MainFrame_Stock")); // NOI18N jPanel1.add(jLabel1); jPanel8.add(jPanel1, java.awt.BorderLayout.NORTH); jPanel10.setPreferredSize(new java.awt.Dimension(328, 170)); jPanel10.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT, 10, 5)); jPanel3.setBackground(new java.awt.Color(255, 255, 255)); jPanel3.setPreferredSize(new java.awt.Dimension(170, 160)); jPanel3.setBorder(new org.jdesktop.swingx.border.DropShadowBorder(true)); jPanel3.setLayout(new java.awt.BorderLayout()); jPanel10.add(jPanel3); EMPTY_DYNAMIC_CHART.getChartPanel().addMouseListener(dynamicChartMouseAdapter); jPanel3.add(EMPTY_DYNAMIC_CHART.getChartPanel(), java.awt.BorderLayout.CENTER); jPanel8.add(jPanel10, java.awt.BorderLayout.SOUTH); jTabbedPane1.addTab(bundle.getString("MainFrame_Title"), jPanel8); // NOI18N getContentPane().add(jTabbedPane1, java.awt.BorderLayout.CENTER); jPanel2.setLayout(new java.awt.GridLayout(2, 1)); getContentPane().add(jPanel2, java.awt.BorderLayout.NORTH); jMenu3.setText(bundle.getString("MainFrame_File")); // NOI18N jMenuItem2.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/project_open.png"))); // NOI18N jMenuItem2.setText(bundle.getString("MainFrame_Open...")); // NOI18N jMenuItem2.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem2ActionPerformed(evt); } }); jMenu3.add(jMenuItem2); jMenuItem9.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/filesave.png"))); // NOI18N jMenuItem9.setText(bundle.getString("MainFrame_SaveAs...")); // NOI18N jMenuItem9.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem9ActionPerformed(evt); } }); jMenu3.add(jMenuItem9); jMenu3.add(jSeparator7); jMenuItem11.setIcon( new javax.swing.ImageIcon(getClass().getResource("/images/16x16/download_from_cloud.png"))); // NOI18N jMenuItem11.setText(bundle.getString("MainFrame_OpenFromCloud...")); // NOI18N jMenuItem11.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem11ActionPerformed(evt); } }); jMenu3.add(jMenuItem11); jMenuItem10.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/upload_to_cloud.png"))); // NOI18N jMenuItem10.setText(bundle.getString("MainFrame_SaveToCloud...")); // NOI18N jMenuItem10.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem10ActionPerformed(evt); } }); jMenu3.add(jMenuItem10); jMenu3.add(jSeparator8); jMenuItem1.setText(bundle.getString("MainFrame_Exit")); // NOI18N jMenuItem1.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem1ActionPerformed(evt); } }); jMenu3.add(jMenuItem1); jMenuBar2.add(jMenu3); jMenu5.setText(bundle.getString("MainFrame_Edit")); // NOI18N jMenuItem4.setText(bundle.getString("MainFrame_AddStocks...")); // NOI18N jMenuItem4.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem4ActionPerformed(evt); } }); jMenu5.add(jMenuItem4); jMenuItem7.setText(bundle.getString("MainFrame_ClearAllStocks")); // NOI18N jMenuItem7.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem7ActionPerformed(evt); } }); jMenu5.add(jMenuItem7); jMenu5.add(jSeparator4); jMenuItem15.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_R, java.awt.event.InputEvent.CTRL_MASK)); jMenuItem15.setText(bundle.getString("MainFrame_RefreshStockPrices")); // NOI18N jMenuItem15.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem15ActionPerformed(evt); } }); jMenu5.add(jMenuItem15); jMenuBar2.add(jMenu5); jMenu6.setText(bundle.getString("MainFrame_Country")); // NOI18N jMenu6.addMenuListener(new javax.swing.event.MenuListener() { public void menuCanceled(javax.swing.event.MenuEvent evt) { } public void menuDeselected(javax.swing.event.MenuEvent evt) { } public void menuSelected(javax.swing.event.MenuEvent evt) { jMenu6MenuSelected(evt); } }); jMenuBar2.add(jMenu6); jMenu10.setText(bundle.getString("MainFrame_Language")); // NOI18N buttonGroup3.add(jRadioButtonMenuItem1); jRadioButtonMenuItem1.setSelected(true); jRadioButtonMenuItem1.setText(Locale.ENGLISH.getDisplayLanguage(Locale.getDefault())); jRadioButtonMenuItem1.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jRadioButtonMenuItem1ActionPerformed(evt); } }); jMenu10.add(jRadioButtonMenuItem1); buttonGroup3.add(jRadioButtonMenuItem2); jRadioButtonMenuItem2.setText(Locale.SIMPLIFIED_CHINESE.getDisplayName(Locale.getDefault())); jRadioButtonMenuItem2.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jRadioButtonMenuItem2ActionPerformed(evt); } }); jMenu10.add(jRadioButtonMenuItem2); buttonGroup3.add(jRadioButtonMenuItem4); jRadioButtonMenuItem4.setText(Locale.TRADITIONAL_CHINESE.getDisplayName(Locale.getDefault())); jRadioButtonMenuItem4.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jRadioButtonMenuItem4ActionPerformed(evt); } }); jMenu10.add(jRadioButtonMenuItem4); buttonGroup3.add(jRadioButtonMenuItem6); jRadioButtonMenuItem6.setText(Locale.FRENCH.getDisplayLanguage(Locale.getDefault())); jRadioButtonMenuItem6.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jRadioButtonMenuItem6ActionPerformed(evt); } }); jMenu10.add(jRadioButtonMenuItem6); buttonGroup3.add(jRadioButtonMenuItem3); jRadioButtonMenuItem3.setText(Locale.GERMAN.getDisplayLanguage(Locale.getDefault())); jRadioButtonMenuItem3.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jRadioButtonMenuItem3ActionPerformed(evt); } }); jMenu10.add(jRadioButtonMenuItem3); buttonGroup3.add(jRadioButtonMenuItem5); jRadioButtonMenuItem5.setText(Locale.ITALIAN.getDisplayLanguage(Locale.getDefault())); jRadioButtonMenuItem5.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jRadioButtonMenuItem5ActionPerformed(evt); } }); jMenu10.add(jRadioButtonMenuItem5); jMenuBar2.add(jMenu10); jMenu7.setText(bundle.getString("MainFrame_Database")); // NOI18N jMenuItem8.setText(bundle.getString("MainFrame_StockDatabase...")); // NOI18N jMenuItem8.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem8ActionPerformed(evt); } }); jMenu7.add(jMenuItem8); jMenuBar2.add(jMenu7); jMenu9.setText(bundle.getString("MainFrame_Watchlist")); // NOI18N jMenu9.addMenuListener(new javax.swing.event.MenuListener() { public void menuCanceled(javax.swing.event.MenuEvent evt) { } public void menuDeselected(javax.swing.event.MenuEvent evt) { } public void menuSelected(javax.swing.event.MenuEvent evt) { jMenu9MenuSelected(evt); } }); jMenuBar2.add(jMenu9); jMenu8.setText(bundle.getString("MainFrame_Portfolio")); // NOI18N jMenu8.addMenuListener(new javax.swing.event.MenuListener() { public void menuCanceled(javax.swing.event.MenuEvent evt) { } public void menuDeselected(javax.swing.event.MenuEvent evt) { } public void menuSelected(javax.swing.event.MenuEvent evt) { jMenu8MenuSelected(evt); } }); jMenuBar2.add(jMenu8); jMenu1.setText(bundle.getString("MainFrame_Options")); // NOI18N jMenuItem6.setText(bundle.getString("MainFrame_Options...")); // NOI18N jMenuItem6.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem6ActionPerformed(evt); } }); jMenu1.add(jMenuItem6); jMenuBar2.add(jMenu1); jMenu4.setText(bundle.getString("MainFrame_LooknFeel")); // NOI18N jMenuBar2.add(jMenu4); jMenu2.setText(bundle.getString("MainFrame_Help")); // NOI18N jMenuItem3.setText(bundle.getString("MainFrame_OnlineHelp")); // NOI18N jMenuItem3.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem3ActionPerformed(evt); } }); jMenu2.add(jMenuItem3); jMenuItem16.setText(bundle.getString("MainFrame_KeyboardShortcuts")); // NOI18N jMenuItem16.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem16ActionPerformed(evt); } }); jMenu2.add(jMenuItem16); jMenuItem12.setText(bundle.getString("MainFrame_Calculator")); // NOI18N jMenuItem12.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem12ActionPerformed(evt); } }); jMenu2.add(jMenuItem12); jMenu2.add(jSeparator6); jMenuItem13.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/smile2.png"))); // NOI18N jMenuItem13.setText(bundle.getString("MainFrame_DonateToJStock")); // NOI18N jMenuItem13.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem13ActionPerformed(evt); } }); jMenu2.add(jMenuItem13); jMenuItem14.setText(bundle.getString("MainFrame_ContributeToJStock")); // NOI18N jMenuItem14.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem14ActionPerformed(evt); } }); jMenu2.add(jMenuItem14); jMenu2.add(jSeparator5); jMenuItem5.setText(bundle.getString("MainFrame_About...")); // NOI18N jMenuItem5.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem5ActionPerformed(evt); } }); jMenu2.add(jMenuItem5); jMenuBar2.add(jMenu2); jMenu11.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/android-small.png"))); // NOI18N jMenu11.setText(bundle.getString("MainFrame_Android")); // NOI18N jMenu11.setFont(jMenu11.getFont().deriveFont(jMenu11.getFont().getStyle() | java.awt.Font.BOLD)); jMenuItem17.setText(bundle.getString("MainFrame_DownloadJStockAndroid")); // NOI18N jMenuItem17.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jMenuItem17ActionPerformed(evt); } }); jMenu11.add(jMenuItem17); jMenuBar2.add(jMenu11); setJMenuBar(jMenuBar2); setSize(new java.awt.Dimension(952, 478)); setLocationRelativeTo(null); }// </editor-fold>//GEN-END:initComponents private void jMenuItem4ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem4ActionPerformed if (this.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; } StockJDialog stockJDialog = new StockJDialog(this, true); stockJDialog.setLocationRelativeTo(this); stockJDialog.setVisible(true); }//GEN-LAST:event_jMenuItem4ActionPerformed private void jMenuItem7ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem7ActionPerformed this.clearAllStocks(); }//GEN-LAST:event_jMenuItem7ActionPerformed private boolean openAsCSVFile(File file) { final Statements statements = Statements.newInstanceFromCSVFile(file); return this.openAsStatements(statements, file); } public boolean openAsStatements(Statements statements, File file) { assert (statements != null); final GUIBundleWrapper guiBundleWrapper = statements.getGUIBundleWrapper(); if (statements.getType() == Statement.Type.RealtimeInfo) { final int size = statements.size(); for (int i = 0; i < size; i++) { final org.yccheok.jstock.file.Statement statement = statements.get(i); final String codeStr = statement.getValueAsString(guiBundleWrapper.getString("MainFrame_Code")); final String symbolStr = statement.getValueAsString(guiBundleWrapper.getString("MainFrame_Symbol")); final Double fallBelowDouble = statement .getValueAsDouble(guiBundleWrapper.getString("MainFrame_FallBelow")); final Double riseAboveDouble = statement .getValueAsDouble(guiBundleWrapper.getString("MainFrame_RiseAbove")); if (codeStr.length() > 0 && symbolStr.length() > 0) { final Stock stock = org.yccheok.jstock.engine.Utils.getEmptyStock(Code.newInstance(codeStr), Symbol.newInstance(symbolStr)); final StockAlert stockAlert = new StockAlert().setFallBelow(fallBelowDouble) .setRiseAbove(riseAboveDouble); this.addStockToTable(stock, stockAlert); realTimeStockMonitor.addStockCode(Code.newInstance(codeStr)); } } realTimeStockMonitor.startNewThreadsIfNecessary(); realTimeStockMonitor.refresh(); } else if (statements.getType() == Statement.Type.StockIndicatorScanner) { // Some users request of having Stock Watchlist able to load stocks // saved from Stock Indicators Scanner. final int size = statements.size(); for (int i = 0; i < size; i++) { final org.yccheok.jstock.file.Statement statement = statements.get(i); final String codeStr = statement.getValueAsString(guiBundleWrapper.getString("MainFrame_Code")); final String symbolStr = statement.getValueAsString(guiBundleWrapper.getString("MainFrame_Symbol")); if (codeStr.length() > 0 && symbolStr.length() > 0) { final Stock stock = org.yccheok.jstock.engine.Utils.getEmptyStock(Code.newInstance(codeStr), Symbol.newInstance(symbolStr)); this.addStockToTable(stock); realTimeStockMonitor.addStockCode(Code.newInstance(codeStr)); } } realTimeStockMonitor.startNewThreadsIfNecessary(); realTimeStockMonitor.refresh(); } else if (statements.getType() == Statement.Type.PortfolioManagementBuy || statements.getType() == Statement.Type.PortfolioManagementSell || statements.getType() == Statement.Type.PortfolioManagementDeposit || statements.getType() == Statement.Type.PortfolioManagementDividend) { /* Open using other tabs. */ return this.portfolioManagementJPanel.openAsStatements(statements, file); } else { return false; } return true; } private boolean openAsExcelFile(File file) { final java.util.List<Statements> statementsList = Statements.newInstanceFromExcelFile(file); boolean status = statementsList.size() > 0; for (Statements statements : statementsList) { status = status & this.openAsStatements(statements, file); } return status; } public RealTimeStockMonitor getRealTimeStockMonitor() { return realTimeStockMonitor; } private void jMenuItem2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem2ActionPerformed final File file = Utils.promptOpenCSVAndExcelJFileChooser(); if (file == null) { return; } boolean status = true; if (Utils.getFileExtension(file).equals("xls")) { if (this.getSelectedComponent() == this.jPanel8) { status = this.openAsExcelFile(file); } else if (this.getSelectedComponent() == this.portfolioManagementJPanel) { status = this.portfolioManagementJPanel.openAsExcelFile(file); } else { assert (false); } } else if (Utils.getFileExtension(file).equals("csv")) { if (this.getSelectedComponent() == this.jPanel8) { status = this.openAsCSVFile(file); } else if (this.getSelectedComponent() == this.portfolioManagementJPanel) { status = this.portfolioManagementJPanel.openAsCSVFile(file); } else { assert (false); } } else { assert (false); } if (false == status) { final String output = MessageFormat .format(MessagesBundle.getString("error_message_bad_file_format_template"), file.getName()); JOptionPane.showMessageDialog(this, output, MessagesBundle.getString("error_title_bad_file_format"), JOptionPane.ERROR_MESSAGE); } }//GEN-LAST:event_jMenuItem2ActionPerformed private void handleJTabbedPaneStateChanged(JTabbedPane pane) { // MainFrame if (pane.getSelectedComponent() == this.jPanel8) { this.jMenuItem2.setEnabled(true); // Load this.jMenuItem9.setEnabled(true); // Save this.jMenuItem4.setEnabled(true); // Add Stocks... this.jMenuItem7.setEnabled(true); // Clear All Stocks... this.jMenuItem15.setEnabled(true); // Refresh Stock Prices requestFocusOnJComboBox(); } else if (pane.getSelectedComponent() == this.indicatorPanel) { this.jMenuItem2.setEnabled(false); // Load this.jMenuItem9.setEnabled(false); // Save this.jMenuItem4.setEnabled(false); // Add Stocks... this.jMenuItem7.setEnabled(false); // Clear All Stocks... this.jMenuItem15.setEnabled(false); // Refresh Stock Prices } else if (pane.getSelectedComponent() == this.indicatorScannerJPanel) { this.jMenuItem2.setEnabled(false); // Load this.jMenuItem9.setEnabled(true); // Save this.jMenuItem4.setEnabled(false); // Add Stocks... this.jMenuItem7.setEnabled(false); // Clear All Stocks... this.jMenuItem15.setEnabled(true); // Refresh Stock Prices } else if (pane.getSelectedComponent() == this.portfolioManagementJPanel) { this.jMenuItem2.setEnabled(true); // Load this.jMenuItem9.setEnabled(true); // Save this.jMenuItem4.setEnabled(false); // Add Stocks... this.jMenuItem7.setEnabled(false); // Clear All Stocks... this.jMenuItem15.setEnabled(true); // Refresh Stock Prices } if (this.isStatusBarBusy == false) { this.setStatusBar(false, this.getBestStatusBarMessage()); } } // Policy : Each pane should have their own real time stock monitoring. // // Each pane should share history monitoring with main frame, // for optimized history retrieving purpose. // private void jTabbedPane1StateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_jTabbedPane1StateChanged // TODO add your handling code here: JTabbedPane pane = (JTabbedPane) evt.getSource(); handleJTabbedPaneStateChanged(pane); }//GEN-LAST:event_jTabbedPane1StateChanged private void jMenuItem6ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem6ActionPerformed OptionsJDialog optionsJDialog = new OptionsJDialog(this, true); optionsJDialog.setLocationRelativeTo(this); optionsJDialog.set(jStockOptions); optionsJDialog.setVisible(true); }//GEN-LAST:event_jMenuItem6ActionPerformed /** * Returns JStock options of this main frame. * @return JStock options of this main frame */ public JStockOptions getJStockOptions() { return this.jStockOptions; } /** * Returns the chart dialog options of this main frame. * @return the chart dialog options of this main frame */ public ChartJDialogOptions getChartJDialogOptions() { return this.chartJDialogOptions; } /** * Save the entire application settings. */ public void save() { // Save the last viewed page. this.getJStockOptions().setLastSelectedPageIndex(this.jTabbedPane1.getSelectedIndex()); // Save current window size and position. JStockOptions.BoundsEx boundsEx = new JStockOptions.BoundsEx(this.getBounds(), this.getExtendedState()); this.getJStockOptions().setBoundsEx(boundsEx); jStockOptions.setApplicationVersionID(Utils.getApplicationVersionID()); this.saveJStockOptions(); this.saveGUIOptions(); this.saveChartJDialogOptions(); this.saveWatchlist(); this.indicatorPanel.saveAlertIndicatorProjectManager(); this.indicatorPanel.saveModuleIndicatorProjectManager(); this.portfolioManagementJPanel.savePortfolio(); } /** * Dettach all and stop Ajax threading activity in combo box. Once stop, * this combo box can no longer be reused. */ private void dettachAllAndStopAutoCompleteJComboBox() { // We are no longer interest to receive any event from combo box. ((AutoCompleteJComboBox) this.jComboBox1).dettachAll(); // Stop all threading activities in AutoCompleteJComboBox. ((AutoCompleteJComboBox) this.jComboBox1).stop(); } // windowClosing // Invoked when the user attempts to close the window from the window's system menu. // // windowClosed // Invoked when a window has been closed as the result of calling dispose on the window. // /* Dangerous! We didn't perform proper clean up, because we do not want * to give user perspective that our system is slow. But, is it safe * to do so? */ // Remember to revise installShutdownHook private void formWindowClosed(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosed isFormWindowClosedCalled = true; try { ExecutorService _stockInfoDatabaseMetaPool = this.stockInfoDatabaseMetaPool; this.stockInfoDatabaseMetaPool = null; _stockInfoDatabaseMetaPool.shutdownNow(); // Always be the first statement. As no matter what happen, we must // save all the configuration files. this.save(); if (this.needToSaveUserDefinedDatabase) { // We are having updated user database in memory. // Save it to disk. this.saveUserDefinedDatabaseAsCSV(jStockOptions.getCountry(), stockInfoDatabase); } // Hide the icon immediately. TrayIcon _trayIcon = trayIcon; if (_trayIcon != null) { SystemTray.getSystemTray().remove(_trayIcon); trayIcon = null; } dettachAllAndStopAutoCompleteJComboBox(); this.indicatorPanel.dettachAllAndStopAutoCompleteJComboBox(); log.info("latestNewsTask stop..."); if (this.latestNewsTask != null) { this.latestNewsTask.cancel(true); } _stockInfoDatabaseMetaPool.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); // We suppose to call shutdownAll to clean up all network resources. // However, that will cause Exception in other threads if they are still using httpclient. // Exception in thread "Thread-4" java.lang.IllegalStateException: Connection factory has been shutdown. // // MultiThreadedHttpConnectionManager.shutdownAll(); log.info("Widnow is closed."); } catch (Exception exp) { log.error("Unexpected error while trying to quit application", exp); } Platform.exit(); // All the above operations are done within try block, to ensure // System.exit(0) will always be called. // // Final clean up. System.exit(0); }//GEN-LAST:event_formWindowClosed private void jMenuItem5ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem5ActionPerformed new AboutJDialog(this, true).setVisible(true); }//GEN-LAST:event_jMenuItem5ActionPerformed private void jMenuItem1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem1ActionPerformed this.setVisible(false); this.dispose(); }//GEN-LAST:event_jMenuItem1ActionPerformed private void formWindowIconified(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowIconified // Calling setVisible(false) will cause modal dialog box to be unblocked // for JDialog.setVisible(true). This will happen in Linux system where // user are allowed to minimize window even there is a modal JDialog box // We have no solution at current moment. // if (Utils.isWindows()) { this.setVisible(false); } }//GEN-LAST:event_formWindowIconified private void formWindowDeiconified(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowDeiconified // TODO add your handling code here: }//GEN-LAST:event_formWindowDeiconified private void formMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseClicked this.jTable1.getSelectionModel().clearSelection(); this.indicatorScannerJPanel.clearTableSelection(); this.portfolioManagementJPanel.clearTableSelection(); updateDynamicChart(null); }//GEN-LAST:event_formMouseClicked private void updateDynamicChart(Stock stock) { assert (java.awt.EventQueue.isDispatchThread()); DynamicChart dynamicChart = stock != null ? this.dynamicCharts.get(stock.code) : JStock.EMPTY_DYNAMIC_CHART; if (dynamicChart == null) { dynamicChart = JStock.EMPTY_DYNAMIC_CHART; } if (java.util.Arrays.asList(jPanel3.getComponents()).contains(dynamicChart.getChartPanel())) { return; } this.jPanel3.removeAll(); this.jPanel3.add(dynamicChart.getChartPanel(), java.awt.BorderLayout.CENTER); this.jPanel3.validate(); // Not sure why. validate itself is not enough to perform update. We // must call repaint as well. dynamicChart.getChartPanel().repaint(); dynamicChart.getChartPanel().removeMouseListener(dynamicChartMouseAdapter); dynamicChart.getChartPanel().addMouseListener(dynamicChartMouseAdapter); } private void jTable1KeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_jTable1KeyPressed if (KeyEvent.VK_DELETE == evt.getKeyCode()) { this.deteleSelectedTableRow(); return; } else if (KeyEvent.VK_ENTER == evt.getKeyCode()) { displayHistoryCharts(); return; } else if (evt.isActionKey()) { int[] rows = JStock.this.jTable1.getSelectedRows(); if (rows.length == 1) { int row = rows[0]; final StockTableModel tableModel = (StockTableModel) jTable1.getModel(); final int modelIndex = jTable1.convertRowIndexToModel(row); final Stock stock = tableModel.getStock(modelIndex); this.updateDynamicChart(stock); } else { this.updateDynamicChart(null); } } }//GEN-LAST:event_jTable1KeyPressed private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing if (this.indicatorPanel.promptToSaveSignificantEdits()) { this.dispose(); } }//GEN-LAST:event_formWindowClosing private void jMenuItem8ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem8ActionPerformed // Use local variable to ensure thread safety. final StockInfoDatabase stock_info_database = this.stockInfoDatabase; if (stock_info_database == null) { JOptionPane.showMessageDialog(this, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages") .getString("info_message_there_are_no_database_ready_yet"), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages").getString( "info_title_there_are_no_database_ready_yet"), JOptionPane.INFORMATION_MESSAGE); return; } // stockDatabaseJDialog will be calling mutable methods of // stock_info_database. StockDatabaseJDialog stockDatabaseJDialog = new StockDatabaseJDialog(this, stock_info_database, true); stockDatabaseJDialog.setSize(540, 540); stockDatabaseJDialog.setLocationRelativeTo(this); stockDatabaseJDialog.setVisible(true); if (stockDatabaseJDialog.getResult() != null) { assert (stockDatabaseJDialog.getResult() == this.stockInfoDatabase); this.stockInfoDatabase = stockDatabaseJDialog.getResult(); ((AutoCompleteJComboBox) jComboBox1).setStockInfoDatabase(this.stockInfoDatabase); indicatorPanel.setStockInfoDatabase(this.stockInfoDatabase); log.info("saveStockCodeAndSymbolDatabase..."); saveDatabase(); } }//GEN-LAST:event_jMenuItem8ActionPerformed private void jMenuItem9ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem9ActionPerformed String suggestedFileName = ""; if (this.getSelectedComponent() == this.jPanel8) { suggestedFileName = GUIBundle.getString("MainFrame_Title"); } else if (this.getSelectedComponent() == this.indicatorScannerJPanel) { suggestedFileName = GUIBundle.getString("IndicatorScannerJPanel_Title"); } else if (this.getSelectedComponent() == this.portfolioManagementJPanel) { suggestedFileName = GUIBundle.getString("PortfolioManagementJPanel_Title"); } else { assert (false); } boolean status = true; File file = null; if (this.getSelectedComponent() == this.jPanel8 || this.getSelectedComponent() == this.indicatorScannerJPanel) { file = Utils.promptSaveCSVAndExcelJFileChooser(suggestedFileName); if (file != null) { if (Utils.getFileExtension(file).equals("csv")) { if (this.getSelectedComponent() == this.jPanel8) { status = this.saveAsCSVFile(file, false); } else if (this.getSelectedComponent() == this.indicatorScannerJPanel) { status = this.indicatorScannerJPanel.saveAsCSVFile(file); } else { assert (false); } } else if (Utils.getFileExtension(file).equals("xls")) { if (this.getSelectedComponent() == this.jPanel8) { status = this.saveAsExcelFile(file); } else if (this.getSelectedComponent() == this.indicatorScannerJPanel) { status = this.indicatorScannerJPanel.saveAsExcelFile(file); } else { assert (false); } } } } else if (this.getSelectedComponent() == this.portfolioManagementJPanel) { final Utils.FileEx fileEx = Utils.promptSavePortfolioCSVAndExcelJFileChooser(suggestedFileName); if (fileEx != null) { file = fileEx.file; if (Utils.getFileExtension(fileEx.file).equals("csv")) { status = this.portfolioManagementJPanel.saveAsCSVFile(fileEx, false); } else if (Utils.getFileExtension(fileEx.file).equals("xls")) { status = this.portfolioManagementJPanel.saveAsExcelFile(fileEx.file, false); } } } else { assert (false); } if (false == status) { // file will never become null, if status had been changed from true // to false. assert (file != null); final String output = MessageFormat .format(MessagesBundle.getString("error_message_nothing_to_be_saved_template"), file.getName()); JOptionPane.showMessageDialog(this, output, MessagesBundle.getString("error_title_nothing_to_be_saved"), JOptionPane.ERROR_MESSAGE); } }//GEN-LAST:event_jMenuItem9ActionPerformed private void jMenu8MenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_jMenu8MenuSelected this.jMenu8.removeAll(); final java.util.List<String> portfolioNames = org.yccheok.jstock.portfolio.Utils.getPortfolioNames(); final String currentPortfolioName = this.getJStockOptions().getPortfolioName(); final javax.swing.ButtonGroup buttonGroup = new javax.swing.ButtonGroup(); for (String portfolioName : portfolioNames) { final JMenuItem mi = (JRadioButtonMenuItem) jMenu8.add(new JRadioButtonMenuItem(portfolioName)); buttonGroup.add(mi); mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { final String s = ((JRadioButtonMenuItem) e.getSource()).getText(); if (false == s.equals(currentPortfolioName)) { JStock.this.selectActivePortfolio(s); } } }); mi.setSelected(portfolioName.equals(currentPortfolioName)); } jMenu8.addSeparator(); final JMenuItem mi = new JMenuItem(GUIBundle.getString("MainFrame_MultiplePortolio..."), this.getImageIcon("/images/16x16/calc.png")); mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { multiplePortfolios(); } }); jMenu8.add(mi); }//GEN-LAST:event_jMenu8MenuSelected private void jMenuItem11ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem11ActionPerformed loadFromCloud(); }//GEN-LAST:event_jMenuItem11ActionPerformed public void saveToCloud() { jMenu3.setEnabled(false); SwingWorker swingWorker = new SwingWorker<Pair<Pair<Credential, String>, Boolean>, Void>() { @Override protected Pair<Pair<Credential, String>, Boolean> doInBackground() throws Exception { final Pair<Pair<Credential, String>, Boolean> pair = org.yccheok.jstock.google.Utils .authorizeDrive(); return pair; } @Override public void done() { jMenu3.setEnabled(true); Pair<Pair<Credential, String>, Boolean> pair = null; try { pair = this.get(); } catch (InterruptedException ex) { JOptionPane.showMessageDialog(JStock.this, ex.getMessage(), GUIBundle.getString("SaveToCloudJDialog_Title"), JOptionPane.ERROR_MESSAGE); log.error(null, ex); } catch (ExecutionException ex) { org.yccheok.jstock.google.Utils.logoutDrive(); JOptionPane.showMessageDialog(JStock.this, ex.getMessage(), GUIBundle.getString("SaveToCloudJDialog_Title"), JOptionPane.ERROR_MESSAGE); log.error(null, ex); } if (pair == null) { return; } SaveToCloudJDialog saveToCloudJDialog = new SaveToCloudJDialog(JStock.this, true, pair.first, pair.second); saveToCloudJDialog.setVisible(true); } }; swingWorker.execute(); } public void loadFromCloud() { jMenu3.setEnabled(false); SwingWorker swingWorker = new SwingWorker<Pair<Pair<Credential, String>, Boolean>, Void>() { @Override protected Pair<Pair<Credential, String>, Boolean> doInBackground() throws Exception { final Pair<Pair<Credential, String>, Boolean> pair = org.yccheok.jstock.google.Utils .authorizeDrive(); if (pair == null) { return null; } return pair; } @Override public void done() { jMenu3.setEnabled(true); Pair<Pair<Credential, String>, Boolean> pair = null; try { pair = this.get(); } catch (InterruptedException ex) { JOptionPane.showMessageDialog(JStock.this, ex.getMessage(), GUIBundle.getString("LoadFromCloudJDialog_Title"), JOptionPane.ERROR_MESSAGE); log.error(null, ex); } catch (ExecutionException ex) { org.yccheok.jstock.google.Utils.logoutDrive(); JOptionPane.showMessageDialog(JStock.this, ex.getMessage(), GUIBundle.getString("LoadFromCloudJDialog_Title"), JOptionPane.ERROR_MESSAGE); log.error(null, ex); } if (pair == null) { return; } LoadFromCloudJDialog loadFromCloudJDialog = new LoadFromCloudJDialog(JStock.this, true, pair.first, pair.second); loadFromCloudJDialog.setVisible(true); } }; swingWorker.execute(); } private void jMenuItem10ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem10ActionPerformed saveToCloud(); }//GEN-LAST:event_jMenuItem10ActionPerformed private void jMenuItem3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem3ActionPerformed Utils.launchWebBrowser( org.yccheok.jstock.network.Utils.getURL(org.yccheok.jstock.network.Utils.Type.HELP_HTML)); }//GEN-LAST:event_jMenuItem3ActionPerformed private void jMenu9MenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_jMenu9MenuSelected this.jMenu9.removeAll(); final java.util.List<String> watchlistNames = org.yccheok.jstock.watchlist.Utils.getWatchlistNames(); final String currentWatchlistName = this.getJStockOptions().getWatchlistName(); final javax.swing.ButtonGroup buttonGroup = new javax.swing.ButtonGroup(); for (String watchlistName : watchlistNames) { final JMenuItem mi = (JRadioButtonMenuItem) this.jMenu9.add(new JRadioButtonMenuItem(watchlistName)); buttonGroup.add(mi); mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { final String s = ((JRadioButtonMenuItem) e.getSource()).getText(); if (false == s.equals(currentWatchlistName)) { JStock.this.selectActiveWatchlist(s); } } }); mi.setSelected(watchlistName.equals(currentWatchlistName)); } this.jMenu9.addSeparator(); final JMenuItem mi = new JMenuItem(GUIBundle.getString("MainFrame_MultipleWatchlist..."), this.getImageIcon("/images/16x16/stock_timezone.png")); mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { multipleWatchlists(); } }); this.jMenu9.add(mi); }//GEN-LAST:event_jMenu9MenuSelected private void jMenuItem12ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem12ActionPerformed try { Runtime.getRuntime().exec("calc"); } catch (IOException ex) { log.error(null, ex); // External program not found. Use our own calculator. org.yccheok.jstock.gui.portfolio.Calc calc = new org.yccheok.jstock.gui.portfolio.Calc(this, false); calc.setVisible(true); } }//GEN-LAST:event_jMenuItem12ActionPerformed private void jRadioButtonMenuItem1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem1ActionPerformed if (false == org.yccheok.jstock.gui.Utils.hasSpecifiedLanguageFile(this.jStockOptions.getLocale())) { // User is currently using default langauge. English is our default // langauge. Hence, do nothing and return early. This is because we // want to avoid from having the following locale. // // Locale(ENGLISH, FRANCE) // // This will yield incorrect behavior during currency formatting. // We prefer to have // // Locale(FRANCE, FRANCE) // // English language will be displayed still, as we do not have // FRANCE language file yet. // return; } // Avoid from Confirm Dialog to pop up when user change to same language (i.e. english) if (false == this.jStockOptions.getLocale().getLanguage().equals(Locale.ENGLISH.getLanguage())) { // Do not suprise user with sudden restart. Ask for their permission to do so. final int result = JOptionPane.showConfirmDialog(this, MessagesBundle.getString("question_message_restart_now"), MessagesBundle.getString("question_title_restart_now"), JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { final Locale locale = new Locale(Locale.ENGLISH.getLanguage(), Locale.getDefault().getCountry(), Locale.getDefault().getVariant()); this.jStockOptions.setLocale(locale); org.yccheok.jstock.gui.Utils.restartApplication(this); } // return to the previous selection if the user press "no" in the dialog else { if (Utils.isTraditionalChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem4.setSelected(true); } else if (Utils.isSimplifiedChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem2.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.GERMAN.getLanguage()) == 0) { this.jRadioButtonMenuItem3.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.ITALIAN.getLanguage()) == 0) { this.jRadioButtonMenuItem5.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.FRENCH.getLanguage()) == 0) { this.jRadioButtonMenuItem6.setSelected(true); } } } }//GEN-LAST:event_jRadioButtonMenuItem1ActionPerformed private void jRadioButtonMenuItem2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem2ActionPerformed // Avoid from Confirm Dialog to pop up when user change to same language (i.e. simplified chinese) if (false == Utils.isSimplifiedChinese(this.jStockOptions.getLocale())) { // Do not suprise user with sudden restart. Ask for their permission to do so. final int result = JOptionPane.showConfirmDialog(this, MessagesBundle.getString("question_message_restart_now"), MessagesBundle.getString("question_title_restart_now"), JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { String country = Locale.TRADITIONAL_CHINESE.getCountry().equals(Locale.getDefault().getCountry()) ? Locale.SIMPLIFIED_CHINESE.getCountry() : Locale.getDefault().getCountry(); final Locale locale = new Locale(Locale.SIMPLIFIED_CHINESE.getLanguage(), country, Locale.getDefault().getVariant()); this.jStockOptions.setLocale(locale); org.yccheok.jstock.gui.Utils.restartApplication(this); } // return to the previous selection if the user press "no" in the dialog else { if (this.jStockOptions.getLocale().getLanguage().compareTo(Locale.ENGLISH.getLanguage()) == 0) { this.jRadioButtonMenuItem1.setSelected(true); } else if (Utils.isTraditionalChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem4.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.GERMAN.getLanguage()) == 0) { this.jRadioButtonMenuItem3.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.ITALIAN.getLanguage()) == 0) { this.jRadioButtonMenuItem5.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.FRENCH.getLanguage()) == 0) { this.jRadioButtonMenuItem6.setSelected(true); } } } }//GEN-LAST:event_jRadioButtonMenuItem2ActionPerformed private void jRadioButtonMenuItem3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem3ActionPerformed // Avoid from Confirm Dialog to pop up when user change to same language (i.e. german) if (false == this.jStockOptions.getLocale().getLanguage().equals(Locale.GERMAN.getLanguage())) { // Do not suprise user with sudden restart. Ask for their permission to do so. final int result = JOptionPane.showConfirmDialog(this, MessagesBundle.getString("question_message_restart_now"), MessagesBundle.getString("question_title_restart_now"), JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { final Locale locale = new Locale(Locale.GERMAN.getLanguage(), Locale.getDefault().getCountry(), Locale.getDefault().getVariant()); this.jStockOptions.setLocale(locale); org.yccheok.jstock.gui.Utils.restartApplication(this); } // return to the previous selection if the user press "no" in the dialog else { if (this.jStockOptions.getLocale().getLanguage().compareTo(Locale.ENGLISH.getLanguage()) == 0) { this.jRadioButtonMenuItem1.setSelected(true); } else if (Utils.isTraditionalChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem4.setSelected(true); } else if (Utils.isSimplifiedChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem2.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.ITALIAN.getLanguage()) == 0) { this.jRadioButtonMenuItem5.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.FRENCH.getLanguage()) == 0) { this.jRadioButtonMenuItem6.setSelected(true); } } } }//GEN-LAST:event_jRadioButtonMenuItem3ActionPerformed private void jRadioButtonMenuItem4ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem4ActionPerformed // Avoid from Confirm Dialog to pop up when user change to same language (i.e. german) if (false == Utils.isTraditionalChinese(this.jStockOptions.getLocale())) { // Do not suprise user with sudden restart. Ask for their permission to do so. final int result = JOptionPane.showConfirmDialog(this, MessagesBundle.getString("question_message_restart_now"), MessagesBundle.getString("question_title_restart_now"), JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { // Unline simplified chinese, we will not use Locale.getDefault().getCountry(). // Instead, we will be using Locale.TRADITIONAL_CHINESE.getCountry(). final Locale locale = new Locale(Locale.TRADITIONAL_CHINESE.getLanguage(), Locale.TRADITIONAL_CHINESE.getCountry(), Locale.getDefault().getVariant()); this.jStockOptions.setLocale(locale); org.yccheok.jstock.gui.Utils.restartApplication(this); } // return to the previous selection if the user press "no" in the dialog else { if (this.jStockOptions.getLocale().getLanguage().compareTo(Locale.ENGLISH.getLanguage()) == 0) { this.jRadioButtonMenuItem1.setSelected(true); } else if (Utils.isSimplifiedChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem2.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.GERMAN.getLanguage()) == 0) { this.jRadioButtonMenuItem3.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.ITALIAN.getLanguage()) == 0) { this.jRadioButtonMenuItem5.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.FRENCH.getLanguage()) == 0) { this.jRadioButtonMenuItem6.setSelected(true); } } } }//GEN-LAST:event_jRadioButtonMenuItem4ActionPerformed private void jMenuItem13ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem13ActionPerformed Utils.launchWebBrowser( org.yccheok.jstock.network.Utils.getURL(org.yccheok.jstock.network.Utils.Type.DONATE_HTML)); }//GEN-LAST:event_jMenuItem13ActionPerformed private void jMenuItem14ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem14ActionPerformed Utils.launchWebBrowser( org.yccheok.jstock.network.Utils.getURL(org.yccheok.jstock.network.Utils.Type.CONTRIBUTE_HTML)); }//GEN-LAST:event_jMenuItem14ActionPerformed private void jRadioButtonMenuItem5ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem5ActionPerformed // Avoid from Confirm Dialog to pop up when user change to same language (i.e. german) if (false == this.jStockOptions.getLocale().getLanguage().equals(Locale.ITALIAN.getLanguage())) { // Do not suprise user with sudden restart. Ask for their permission to do so. final int result = JOptionPane.showConfirmDialog(this, MessagesBundle.getString("question_message_restart_now"), MessagesBundle.getString("question_title_restart_now"), JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { final Locale locale = new Locale(Locale.ITALIAN.getLanguage(), Locale.getDefault().getCountry(), Locale.getDefault().getVariant()); this.jStockOptions.setLocale(locale); org.yccheok.jstock.gui.Utils.restartApplication(this); } // return to the previous selection if the user press "no" in the dialog else { if (this.jStockOptions.getLocale().getLanguage().compareTo(Locale.ENGLISH.getLanguage()) == 0) { this.jRadioButtonMenuItem1.setSelected(true); } else if (Utils.isTraditionalChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem4.setSelected(true); } else if (Utils.isSimplifiedChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem2.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.GERMAN.getLanguage()) == 0) { this.jRadioButtonMenuItem3.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.FRENCH.getLanguage()) == 0) { this.jRadioButtonMenuItem6.setSelected(true); } } } }//GEN-LAST:event_jRadioButtonMenuItem5ActionPerformed private void jMenuItem15ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem15ActionPerformed refreshAllRealTimeStockMonitors(); refreshRealTimeIndexMonitor(); refreshExchangeRateMonitor(); // Only update UI when there is at least one stock. if (this.getStocks().isEmpty() == false) { this.setStatusBar(true, GUIBundle.getString("MainFrame_RefreshStockPrices...")); refreshPriceInProgress = true; } }//GEN-LAST:event_jMenuItem15ActionPerformed private void jMenuItem16ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem16ActionPerformed Utils.launchWebBrowser(org.yccheok.jstock.network.Utils .getURL(org.yccheok.jstock.network.Utils.Type.HELP_KEYBOARD_SHORTCUTS_HTML)); }//GEN-LAST:event_jMenuItem16ActionPerformed private void jMenuItem17ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem17ActionPerformed Utils.launchWebBrowser( org.yccheok.jstock.network.Utils.getURL(org.yccheok.jstock.network.Utils.Type.ANDROID_HTML)); }//GEN-LAST:event_jMenuItem17ActionPerformed private void jRadioButtonMenuItem6ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonMenuItem6ActionPerformed // Avoid from Confirm Dialog to pop up when user change to same language (i.e. german) if (false == this.jStockOptions.getLocale().getLanguage().equals(Locale.FRENCH.getLanguage())) { // Do not suprise user with sudden restart. Ask for their permission to do so. final int result = JOptionPane.showConfirmDialog(this, MessagesBundle.getString("question_message_restart_now"), MessagesBundle.getString("question_title_restart_now"), JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { final Locale locale = new Locale(Locale.FRENCH.getLanguage(), Locale.getDefault().getCountry(), Locale.getDefault().getVariant()); this.jStockOptions.setLocale(locale); org.yccheok.jstock.gui.Utils.restartApplication(this); } // return to the previous selection if the user press "no" in the dialog else { if (this.jStockOptions.getLocale().getLanguage().compareTo(Locale.ENGLISH.getLanguage()) == 0) { this.jRadioButtonMenuItem1.setSelected(true); } else if (Utils.isTraditionalChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem4.setSelected(true); } else if (Utils.isSimplifiedChinese(this.jStockOptions.getLocale())) { this.jRadioButtonMenuItem2.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.ITALIAN.getLanguage()) == 0) { this.jRadioButtonMenuItem5.setSelected(true); } else if (this.jStockOptions.getLocale().getLanguage() .compareTo(Locale.GERMAN.getLanguage()) == 0) { this.jRadioButtonMenuItem3.setSelected(true); } } } }//GEN-LAST:event_jRadioButtonMenuItem6ActionPerformed private void jMenu6MenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_jMenu6MenuSelected initRecentCountryMenuItems(); }//GEN-LAST:event_jMenu6MenuSelected /** * Activate specified watchlist. * * @param watchlist Watchlist name */ public void selectActiveWatchlist(String watchlist) { assert (SwingUtilities.isEventDispatchThread()); // Save current watchlist. JStock.this.saveWatchlist(); // Save current GUI options. // Do not call MainFrame.this.saveGUIOptions() (Pay note on the underscore) // , as that will save portfolio's and indicator scanner's as well. JStock.this._saveGUIOptions(); // And switch to new portfolio. JStock.this.getJStockOptions().setWatchlistName(watchlist); JStock.this.initWatchlist(); // I guess user wants to watch the current active watchlist right now. // We will help him to turn to the stock watchlist page. JStock.this.jTabbedPane1.setSelectedIndex(0); // No matter how, just stop progress bar, and display best message. this.setStatusBar(false, this.getBestStatusBarMessage()); } /** * Activate specified portfolio. * * @param portfolio Portfolio name */ public void selectActivePortfolio(String portfolio) { assert (SwingUtilities.isEventDispatchThread()); // Save current portfolio. JStock.this.portfolioManagementJPanel.savePortfolio(); // Save current GUI options. JStock.this.portfolioManagementJPanel.saveGUIOptions(); // And switch to new portfolio. JStock.this.getJStockOptions().setPortfolioName(portfolio); JStock.this.portfolioManagementJPanel.initPortfolio(); // I guess user wants to watch the current active portfolio right now. // We will help him to turn to the portfolio page. JStock.this.jTabbedPane1.setSelectedIndex(3); JStock.this.portfolioManagementJPanel.updateTitledBorder(); // No matter how, just stop progress bar, and display best message. this.setStatusBar(false, this.getBestStatusBarMessage()); } private void multipleWatchlists() { WatchlistJDialog watchlistJDialog = new WatchlistJDialog(this, true); watchlistJDialog.setLocationRelativeTo(this); watchlistJDialog.setVisible(true); } private void multiplePortfolios() { PortfolioJDialog portfolioJDialog = new PortfolioJDialog(this, true); portfolioJDialog.setLocationRelativeTo(this); portfolioJDialog.setVisible(true); } private static boolean saveAsCSVFile(CSVWatchlist csvWatchlist, File file, boolean languageIndependent) { final org.yccheok.jstock.file.Statements statements = org.yccheok.jstock.file.Statements .newInstanceFromTableModel(csvWatchlist.tableModel, languageIndependent); assert (statements != null); return statements.saveAsCSVFile(file); } private boolean saveAsCSVFile(File file, boolean languageIndependent) { final TableModel tableModel = jTable1.getModel(); CSVWatchlist csvWatchlist = CSVWatchlist.newInstance(tableModel); return saveAsCSVFile(csvWatchlist, file, languageIndependent); } private boolean saveAsExcelFile(File file) { final TableModel tableModel = jTable1.getModel(); final org.yccheok.jstock.file.Statements statements = org.yccheok.jstock.file.Statements .newInstanceFromTableModel(tableModel, false); assert (statements != null); return statements.saveAsExcelFile(file, GUIBundle.getString("MainFrame_Title")); } private static JStockOptions getJStockOptionsViaXML() { final File f = new File( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "options.xml"); JStockOptions jStockOptions = Utils.fromXML(JStockOptions.class, f); if (jStockOptions == null) { // JStockOptions's file not found. Perhaps this is the first time we // run JStock. jStockOptions = new JStockOptions(); } return jStockOptions; } /** * @param args the command line arguments */ public static void main(String args[]) { /*********************************************************************** * UI Manager initialization via JStockOptions. **********************************************************************/ final JStockOptions jStockOptions = getJStockOptionsViaXML(); // OSX menu bar at top. if (Utils.isMacOSX()) { System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("apple.awt.brushMetalLook", "true"); } boolean uiManagerLookAndFeelSuccess = false; try { String lookNFeel = jStockOptions.getLooknFeel(); if (null != lookNFeel) { UIManager.setLookAndFeel(lookNFeel); uiManagerLookAndFeelSuccess = true; } } catch (java.lang.ClassNotFoundException | java.lang.InstantiationException | java.lang.IllegalAccessException | javax.swing.UnsupportedLookAndFeelException exp) { log.error(null, exp); } if (!uiManagerLookAndFeelSuccess) { String className = Utils.setDefaultLookAndFeel(); if (null != className) { final String lookNFeel = jStockOptions.getLooknFeel(); // When jStockOptions.getLookNFeel returns null, it means we wish // to use system default value. Hence, don't overwrite the null value, // so that we can use the same jStockOptions, across different // platforms. if (lookNFeel != null) { jStockOptions.setLooknFeel(className); } } } /*********************************************************************** * Ensure correct localization. **********************************************************************/ // This global effect, should just come before anything else, // after we get an instance of JStockOptions. Locale.setDefault(jStockOptions.getLocale()); /*********************************************************************** * Single application instance enforcement. **********************************************************************/ if (false == AppLock.lock()) { final int choice = JOptionPane.showOptionDialog(null, MessagesBundle.getString("warning_message_running_2_jstock"), MessagesBundle.getString("warning_title_running_2_jstock"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[] { MessagesBundle.getString("yes_button_running_2_jstock"), MessagesBundle.getString("no_button_running_2_jstock") }, MessagesBundle.getString("no_button_running_2_jstock")); if (choice != JOptionPane.YES_OPTION) { System.exit(0); return; } } // Avoid "JavaFX IllegalStateException when disposing JFXPanel in Swing" // http://stackoverflow.com/questions/16867120/javafx-illegalstateexception-when-disposing-jfxpanel-in-swing Platform.setImplicitExit(false); // As ProxyDetector is affected by system properties // http.proxyHost, we are forced to initialized ProxyDetector right here, // before we manually change the system properties according to // JStockOptions. ProxyDetector.getInstance(); /*********************************************************************** * Apply large font if possible. **********************************************************************/ if (jStockOptions.useLargeFont()) { java.util.Enumeration keys = UIManager.getDefaults().keys(); while (keys.hasMoreElements()) { Object key = keys.nextElement(); Object value = UIManager.get(key); if (value != null && value instanceof javax.swing.plaf.FontUIResource) { javax.swing.plaf.FontUIResource fr = (javax.swing.plaf.FontUIResource) value; UIManager.put(key, new javax.swing.plaf.FontUIResource( fr.deriveFont((float) fr.getSize2D() * (float) Constants.FONT_ENLARGE_FACTOR))); } } } /*********************************************************************** * GA tracking. **********************************************************************/ GA.trackAsynchronously("main"); java.awt.EventQueue.invokeLater(new Runnable() { @Override public void run() { final JStock mainFrame = JStock.instance(); // We need to first assign jStockOptions to mainFrame, as during // Utils.migrateXMLToCSVPortfolios, we will be accessing mainFrame's // jStockOptions. mainFrame.initJStockOptions(jStockOptions); mainFrame.init(); mainFrame.setVisible(true); mainFrame.updateDividerLocation(); mainFrame.requestFocusOnJComboBox(); } }); } // Restore the last saved divider location for portfolio management panel. private void updateDividerLocation() { this.portfolioManagementJPanel.updateDividerLocation(); } private void clearAllStocks() { if (stockCodeHistoryGUI != null) { stockCodeHistoryGUI.clear(); } if (realTimeStockMonitor != null) { realTimeStockMonitor.clearStockCodes(); } if (stockHistoryMonitor != null) { stockHistoryMonitor.clearStockCodes(); } final StockTableModel tableModel = (StockTableModel) jTable1.getModel(); this.initAlertStateManager(); if (java.awt.EventQueue.isDispatchThread()) { tableModel.clearAllStocks(); updateDynamicChart(null); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { tableModel.clearAllStocks(); updateDynamicChart(null); } }); } if (stockCodeHistoryGUI != null) { if (stockCodeHistoryGUI.isEmpty()) { if (this.stockInfoDatabase != null) { this.setStatusBar(false, this.getBestStatusBarMessage()); } } } } // Should we synchronized the jTable1, or post the job at GUI event dispatch // queue? private void deteleSelectedTableRow() { assert (java.awt.EventQueue.isDispatchThread()); StockTableModel tableModel = (StockTableModel) jTable1.getModel(); int rows[] = jTable1.getSelectedRows(); Arrays.sort(rows); for (int i = rows.length - 1; i >= 0; i--) { int row = rows[i]; if (row < 0) { continue; } final int modelIndex = jTable1.getRowSorter().convertRowIndexToModel(row); Stock stock = tableModel.getStock(modelIndex); stockCodeHistoryGUI.remove(stock.code); realTimeStockMonitor.removeStockCode(stock.code); stockHistoryMonitor.removeStockCode(stock.code); tableModel.removeRow(modelIndex); this.alertStateManager.clearState(stock); } this.updateDynamicChart(null); if (stockCodeHistoryGUI.isEmpty()) { if (this.stockInfoDatabase != null) { this.setStatusBar(false, this.getBestStatusBarMessage()); } } } /** * Set the exchange rate value on status bar. * * @param exchangeRate the exchange rate value. null to reset */ public void setStatusBarExchangeRate(final Double exchangeRate) { if (SwingUtilities.isEventDispatchThread()) { statusBar.setExchangeRate(exchangeRate); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { statusBar.setExchangeRate(exchangeRate); } }); } } /** * Set the visibility of exchange rate label on status bar. * * @param visible true to make the exchange rate label visible. Else false */ public void setStatusBarExchangeRateVisible(final boolean visible) { if (SwingUtilities.isEventDispatchThread()) { statusBar.setExchangeRateVisible(visible); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { statusBar.setExchangeRateVisible(visible); } }); } } /** * Set the tool tip text of exchange rate label on status bar. * * @param text the tool tip text */ public void setStatusBarExchangeRateToolTipText(final String text) { if (SwingUtilities.isEventDispatchThread()) { statusBar.setExchangeRateToolTipText(text); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { statusBar.setExchangeRateToolTipText(text); } }); } } /** * Update the status bar. * * @param progressBar true to make progress bar busy. Else false * @param mainMessage message on the left */ public void setStatusBar(final boolean progressBar, final String mainMessage) { if (SwingUtilities.isEventDispatchThread()) { isStatusBarBusy = progressBar; statusBar.setProgressBar(progressBar); statusBar.setMainMessage(mainMessage); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { isStatusBarBusy = progressBar; statusBar.setProgressBar(progressBar); statusBar.setMainMessage(mainMessage); } }); } } class ChangeLookAndFeelAction extends AbstractAction { JStock mainFrame; String lafClassName; protected ChangeLookAndFeelAction(JStock mainFrame, String lafClassName) { super("ChangeTheme"); this.mainFrame = mainFrame; this.lafClassName = lafClassName; } public String getLafClassName() { return lafClassName; } @Override public void actionPerformed(ActionEvent e) { mainFrame.setLookAndFeel(lafClassName); } } private void _setAlwaysOnTop(boolean alwaysOnTop) { this.setAlwaysOnTop(alwaysOnTop); this.jStockOptions.setAlwaysOnTop(alwaysOnTop); this.alwaysOnTopMenuItem.setSelected(alwaysOnTop); } private void setLookAndFeel(String lafClassName) { boolean uiManagerLookAndFeelSuccess = false; String realLafClassName = null; try { if (lafClassName == null) { String className = Utils.setDefaultLookAndFeel(); if (className != null) { SwingUtilities.updateComponentTreeUI(this); realLafClassName = className; uiManagerLookAndFeelSuccess = true; } } else { UIManager.setLookAndFeel(lafClassName); SwingUtilities.updateComponentTreeUI(this); realLafClassName = lafClassName; uiManagerLookAndFeelSuccess = true; } } catch (java.lang.ClassNotFoundException | java.lang.InstantiationException | java.lang.IllegalAccessException | javax.swing.UnsupportedLookAndFeelException exp) { log.error(null, exp); } if (uiManagerLookAndFeelSuccess) { // Don't use realLafClassName. this.jStockOptions.setLooknFeel(lafClassName); for (Enumeration<AbstractButton> e = this.buttonGroup1.getElements(); e.hasMoreElements();) { AbstractButton button = e.nextElement(); javax.swing.JRadioButtonMenuItem m = (javax.swing.JRadioButtonMenuItem) button; ChangeLookAndFeelAction a = (ChangeLookAndFeelAction) m.getActionListeners()[0]; if (a.getLafClassName().equals(realLafClassName)) { m.setSelected(true); break; } } // Sequence are important. The AutoCompleteJComboBox itself should have the highest // priority. ((AutoCompleteJComboBox) jComboBox1).setStockInfoDatabase(this.stockInfoDatabase); this.indicatorPanel.setStockInfoDatabase(this.stockInfoDatabase); } } public PortfolioManagementJPanel getPortfolioManagementJPanel() { return this.portfolioManagementJPanel; } private void createPortfolioManagementJPanel() { portfolioManagementJPanel = new PortfolioManagementJPanel(); jTabbedPane1.addTab(GUIBundle.getString("PortfolioManagementJPanel_Title"), portfolioManagementJPanel); } private void createStockIndicatorEditor() { indicatorPanel = new IndicatorPanel(); jTabbedPane1.addTab(GUIBundle.getString("IndicatorPanel_Title"), indicatorPanel); } private void createIndicatorScannerJPanel() { this.indicatorScannerJPanel = new IndicatorScannerJPanel(); jTabbedPane1.addTab(GUIBundle.getString("IndicatorScannerJPanel_Title"), indicatorScannerJPanel); jTabbedPane1.addChangeListener(indicatorScannerJPanel); } // Due to the unknown problem in netbeans IDE, we will add in the tooltip // and icon seperately. private void createIconsAndToolTipTextForJTabbedPane() { this.jTabbedPane1.setIconAt(0, this.getImageIcon("/images/16x16/stock_timezone.png")); this.jTabbedPane1.setIconAt(1, this.getImageIcon("/images/16x16/color_line.png")); this.jTabbedPane1.setIconAt(2, this.getImageIcon("/images/16x16/find.png")); this.jTabbedPane1.setIconAt(3, this.getImageIcon("/images/16x16/calc.png")); this.jTabbedPane1.setToolTipTextAt(0, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_WatchYourFavoriteStockMovement")); this.jTabbedPane1.setToolTipTextAt(1, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_CustomizeYourOwnStockIndicatorForAlertPurpose")); this.jTabbedPane1.setToolTipTextAt(2, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_ScanThroughTheEntireStockMarketSoThatYouWillBeInformedWhatToSellOrBuy")); this.jTabbedPane1.setToolTipTextAt(3, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_ManageYourRealTimePortfolioWhichEnableYouToTrackBuyAndSellRecords")); } private void initRecentCountryMenuItems() { Enumeration<AbstractButton> e = buttonGroup4.getElements(); boolean hasSeperator = false; while (e.hasMoreElements()) { jMenu6.remove(e.nextElement()); hasSeperator = true; } if (hasSeperator) { // Seperator. jMenu6.remove(0); } buttonGroup4 = new ButtonGroup(); int index = 0; for (final Country country : jStockOptions.getRecentCountries()) { final JMenuItem mi = (JRadioButtonMenuItem) jMenu6 .add(new JRadioButtonMenuItem(country.humanString, country.icon), index++); buttonGroup4.add(mi); mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JStock.this.changeCountry(country); } }); if (jStockOptions.getCountry() == country) { ((JRadioButtonMenuItem) mi).setSelected(true); } } jMenu6.add(new javax.swing.JPopupMenu.Separator(), index++); } public void createCountryMenuItems() { final java.util.List<Country> countries = Utils.getSupportedStockMarketCountries(); Map<Continent, JMenu> menus = new EnumMap<>(Continent.class); for (final Continent continent : Continent.values()) { JMenu jMenu = new JMenu(continent.name()); jMenu6.add(jMenu); menus.put(continent, jMenu); } for (final Country country : countries) { JMenu jMenu = menus.get(Continent.toContinent(country)); // Ugly fix on spelling mistake. final JMenuItem mi = (JRadioButtonMenuItem) jMenu .add(new JRadioButtonMenuItem(country.humanString, country.icon)); buttonGroup2.add(mi); mi.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JStock.this.changeCountry(country); } }); if (jStockOptions.getCountry() == country) { ((JRadioButtonMenuItem) mi).setSelected(true); } } } public void createLookAndFeelMenuItems() { LookAndFeel currentlaf = UIManager.getLookAndFeel(); UIManager.LookAndFeelInfo[] lafInfo = UIManager.getInstalledLookAndFeels(); for (int i = 0; i < lafInfo.length; i++) { JMenuItem mi = (JRadioButtonMenuItem) jMenu4.add(new JRadioButtonMenuItem(lafInfo[i].getName())); buttonGroup1.add(mi); mi.addActionListener(new ChangeLookAndFeelAction(this, lafInfo[i].getClassName())); if (currentlaf != null) { if (lafInfo[i].getClassName().equals(currentlaf.getClass().getName())) { ((JRadioButtonMenuItem) mi).setSelected(true); } } } // Always on Top jMenu4.addSeparator(); this.alwaysOnTopMenuItem = jMenu4.add(new JCheckBoxMenuItem(GUIBundle.getString("MainFrame_AlwaysOnTop"))); this.alwaysOnTopMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { AbstractButton aButton = (AbstractButton) e.getSource(); boolean selected = aButton.getModel().isSelected(); JStock.this._setAlwaysOnTop(selected); } }); } private javax.swing.event.TableModelListener getTableModelListener() { return new javax.swing.event.TableModelListener() { @Override public void tableChanged(javax.swing.event.TableModelEvent e) { int firstRow = e.getFirstRow(); int lastRow = e.getLastRow(); int mColIndex = e.getColumn(); switch (e.getType()) { case javax.swing.event.TableModelEvent.INSERT: break; case javax.swing.event.TableModelEvent.UPDATE: break; case javax.swing.event.TableModelEvent.DELETE: break; } } }; } private Image getMyIconImage() { return new javax.swing.ImageIcon(getClass().getResource("/images/128x128/chart.png")).getImage(); } private void createSystemTrayIcon() { if (SystemTray.isSupported()) { SystemTray tray = SystemTray.getSystemTray(); final Image image = new javax.swing.ImageIcon(getClass().getResource("/images/128x128/chart.png")) .getImage(); MouseListener mouseListener = new MouseListener() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { JStock.this.setVisible(true); JStock.this.setState(Frame.NORMAL); } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } }; ActionListener exitListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JStock.this.setVisible(false); JStock.this.dispose(); } }; PopupMenu popup = new PopupMenu(); MenuItem defaultItem = new MenuItem( java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("MainFrame_Exit")); defaultItem.addActionListener(exitListener); popup.add(defaultItem); trayIcon = new TrayIcon(image, GUIBundle.getString("MainFrame_Application_Title"), popup); ActionListener actionListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { } }; trayIcon.setImageAutoSize(true); trayIcon.addActionListener(actionListener); trayIcon.addMouseListener(mouseListener); try { tray.add(trayIcon); } catch (AWTException e) { trayIcon = null; JOptionPane.showMessageDialog(JStock.this, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages") .getString("warning_message_trayicon_could_not_be_added"), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages").getString( "warning_title_trayicon_could_not_be_added"), JOptionPane.WARNING_MESSAGE); } } else { // System Tray is not supported trayIcon = null; JOptionPane.showMessageDialog(JStock.this, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages") .getString("warning_message_system_tray_is_not_supported"), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages") .getString("warning_title_system_tray_is_not_supported"), JOptionPane.WARNING_MESSAGE); } } private void initTableHeaderToolTips() { JTableHeader header = jTable1.getTableHeader(); ColumnHeaderToolTips tips = new ColumnHeaderToolTips(); tips.setToolTip(jTable1.getColumn(GUIBundle.getString("MainFrame_FallBelow")), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_AlertUserWhenLastPriceFallBelowOrEqualToSpecifiedValue")); tips.setToolTip(jTable1.getColumn(GUIBundle.getString("MainFrame_RiseAbove")), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_AlertUserWhenLastPriceFallAboveOrEqualToSpecifiedValue")); header.addMouseMotionListener(tips); } /* Save everything to disc, before perform uploading. */ public void commitBeforeSaveToCloud() { // Previously, we will store the entire stockcodeandsymboldatabase.xml // to cloud server if stockcodeandsymboldatabase.xml is containing // user defined database. Due to our server is running out of space, we will // only store UserDefined pair. user-defined-database.xml will be only // used for cloud storage purpose. // // The following code is trying to extract user defined database out from // StockCodeAndSymbolDatabase. The code shall be removed after a while, as // this operation will be done in saveDatabase method. It is here merely // for backward compatible purpose. // // We should instead iterate to every Country to save accordingly. However, // it is too time consuming. We will make assumption that user is most // interested in his current viewing country. Hence, for existing user, // he will be having risk of losing other countries UserDefined codes. final Country country = jStockOptions.getCountry(); if (false == Utils.isFileOrDirectoryExist(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database" + File.separator + "user-defined-database.xml")) { final StockInfoDatabase stock_info_database = this.stockInfoDatabase; if (stock_info_database != null) { saveUserDefinedDatabaseAsCSV(country, stock_info_database); } } /* These codes are very similar to clean up code during application * exit. */ jStockOptions.setApplicationVersionID(Utils.getApplicationVersionID()); this.saveJStockOptions(); this.saveGUIOptions(); this.saveChartJDialogOptions(); this.saveWatchlist(); this.indicatorPanel.saveAlertIndicatorProjectManager(); this.indicatorPanel.saveModuleIndicatorProjectManager(); this.portfolioManagementJPanel.savePortfolio(); // In Linux, "My Portfolio" and "my portfolio" are 2 different folders. // However, we cannot commit such folders to cloud. This will cause // problem in Windows. This code was introduced since 1.0.7c. We should // remove it after a while, as we do have 2557 changeset to prevent such // incident in Linux. solveCaseSensitiveFoldersIssue(); saveWatchlistAndPortfolioInfos(); saveBrokingFirmsAsJson(); } private boolean saveBrokingFirmsAsJson() { if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "android") == false) { return false; } GsonBuilder builder = new GsonBuilder(); Gson gson = builder.create(); String string = gson.toJson(this.getJStockOptions().getBrokingFirms()); File brokingFirmsFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "android" + File.separator + "brokingfirms.json"); final ThreadSafeFileLock.Lock lock = ThreadSafeFileLock.getLock(brokingFirmsFile); if (lock == null) { return false; } // http://stackoverflow.com/questions/10868423/lock-lock-before-try ThreadSafeFileLock.lockWrite(lock); try { BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(brokingFirmsFile), "UTF-8")); try { writer.write(string); } finally { writer.close(); } } catch (IOException ex) { log.error(null, ex); return false; } finally { ThreadSafeFileLock.unlockWrite(lock); ThreadSafeFileLock.releaseLock(lock); } return true; } private void solveCaseSensitiveFoldersIssue() { final Country currentCountry = this.jStockOptions.getCountry(); final String currentWatchlist = this.jStockOptions.getWatchlistName(); final String currentPortfolio = this.jStockOptions.getPortfolioName(); //////////////////////////////////////////////////////////////////////// // WATCHLIST //////////////////////////////////////////////////////////////////////// for (Country country : Country.values()) { java.util.List<String> watchlistNames = org.yccheok.jstock.watchlist.Utils.getWatchlistNames(country); Map<String, java.util.List<String>> watchlistNamesMap = new HashMap<String, java.util.List<String>>(); java.util.List<java.util.List<String>> duplicatedNames = new ArrayList<java.util.List<String>>(); Set<String> lowerCaseNames = new HashSet<String>(); for (String watchlistName : watchlistNames) { String lowerCaseWatchlistName = watchlistName.toLowerCase(); lowerCaseNames.add(lowerCaseWatchlistName); java.util.List<String> names = watchlistNamesMap.get(lowerCaseWatchlistName); if (names == null) { names = new ArrayList<String>(); watchlistNamesMap.put(lowerCaseWatchlistName, names); } names.add(watchlistName); if (names.size() > 1) { duplicatedNames.add(names); } } for (java.util.List<String> names : duplicatedNames) { int counter = 0; boolean originalNameUsed = false; for (int i = 0, ei = names.size(); i < ei; i++) { final String originalName = names.get(i); if (currentCountry == country && currentWatchlist.equals(originalName)) { originalNameUsed = true; continue; } String newName = originalName; if (originalNameUsed || i < (ei - 1)) { // Cannot use the original name. newName = originalName + counter++; while (lowerCaseNames.contains(newName.toLowerCase())) { newName = originalName + counter++; } lowerCaseNames.add(newName.toLowerCase()); } else { // Use original name. originalNameUsed = true; } String originalDirectory = org.yccheok.jstock.watchlist.Utils.getWatchlistDirectory(country, originalName); String newDirectory = org.yccheok.jstock.watchlist.Utils.getWatchlistDirectory(country, newName); if (false == originalDirectory.equalsIgnoreCase(newDirectory)) { new File(originalDirectory).renameTo(new File(newDirectory)); } } } } //////////////////////////////////////////////////////////////////////// // PORTFOLIO //////////////////////////////////////////////////////////////////////// for (Country country : Country.values()) { java.util.List<String> portfolioNames = org.yccheok.jstock.portfolio.Utils.getPortfolioNames(country); Map<String, java.util.List<String>> portfolioNamesMap = new HashMap<String, java.util.List<String>>(); java.util.List<java.util.List<String>> duplicatedNames = new ArrayList<java.util.List<String>>(); Set<String> lowerCaseNames = new HashSet<String>(); for (String portfolioName : portfolioNames) { String lowerCasePortfolioName = portfolioName.toLowerCase(); lowerCaseNames.add(lowerCasePortfolioName); java.util.List<String> names = portfolioNamesMap.get(lowerCasePortfolioName); if (names == null) { names = new ArrayList<String>(); portfolioNamesMap.put(lowerCasePortfolioName, names); } names.add(portfolioName); if (names.size() > 1) { duplicatedNames.add(names); } } for (java.util.List<String> names : duplicatedNames) { int counter = 0; boolean originalNameUsed = false; for (int i = 0, ei = names.size(); i < ei; i++) { final String originalName = names.get(i); if (currentCountry == country && currentPortfolio.equals(originalName)) { originalNameUsed = true; continue; } String newName = originalName; if (originalNameUsed || i < (ei - 1)) { // Cannot use the original name. newName = originalName + counter++; while (lowerCaseNames.contains(newName.toLowerCase())) { newName = originalName + counter++; } lowerCaseNames.add(newName.toLowerCase()); } else { // Use original name. originalNameUsed = true; } String originalDirectory = org.yccheok.jstock.portfolio.Utils.getPortfolioDirectory(country, originalName); String newDirectory = org.yccheok.jstock.portfolio.Utils.getPortfolioDirectory(country, newName); if (false == originalDirectory.equalsIgnoreCase(newDirectory)) { new File(originalDirectory).renameTo(new File(newDirectory)); } } } } } // Only call this function after you had saved all the watchlists and // portfolios. private boolean saveWatchlistAndPortfolioInfos() { if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "android") == false) { return false; } java.util.List<WatchlistInfo> watchlistInfos = org.yccheok.jstock.watchlist.Utils.getWatchlistInfos(); java.util.List<PortfolioInfo> portfolioInfos = org.yccheok.jstock.portfolio.Utils.getPortfolioInfos(); File watchlistInfosFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "android" + File.separator + "watchlistinfos.csv"); File portfolioInfosFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "android" + File.separator + "portfolioinfos.csv"); boolean result0 = Statements.newInstanceFromWatchlistInfos(watchlistInfos) .saveAsCSVFile(watchlistInfosFile); boolean result1 = Statements.newInstanceFromPortfolioInfos(portfolioInfos) .saveAsCSVFile(portfolioInfosFile); return result0 && result1; } // Some users complain download from cloud doesn't work. High chance is that, // they are not in correct country. private Country getBestCountryAfterDownloadFromCloud() { final Country country = jStockOptions.getCountry(); Country watchlistCountry = null; Country portfolioCountry = null; int watchlistMaxSize = 0; int portfolioMaxSize = 0; //////////////////////////////////////////////////////////////////////// // PROCESSING WATCHLIST //////////////////////////////////////////////////////////////////////// File watchlistInfosFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "android" + File.separator + "watchlistinfos.csv"); Statements watchlistInfos = Statements.newInstanceFromCSVFile(watchlistInfosFile); final GUIBundleWrapper guiBundleWrapper = GUIBundleWrapper.newInstance(Language.INDEPENDENT); if (watchlistInfos.getType() == Statement.Type.WatchlistInfos) { for (int i = 0, ei = watchlistInfos.size(); i < ei; i++) { Statement statement = watchlistInfos.get(i); String countryString = statement .getValueAsString(guiBundleWrapper.getString("WatchlistInfo_Country")); Country c; try { c = Country.valueOf(countryString); } catch (IllegalArgumentException ex) { log.error(null, ex); continue; } assert (c != null); if (c == country) { return c; } Double _watchlistSize = statement .getValueAsDouble(guiBundleWrapper.getString("WatchlistInfo_Size")); if (_watchlistSize == null) { continue; } int watchlistSize = (int) (double) _watchlistSize; if (watchlistSize > watchlistMaxSize) { watchlistMaxSize = watchlistSize; watchlistCountry = c; } } } //////////////////////////////////////////////////////////////////////// // PROCESSING PORTFOLIO //////////////////////////////////////////////////////////////////////// File portfolioInfosFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "android" + File.separator + "portfolioinfos.csv"); Statements portfolioInfos = Statements.newInstanceFromCSVFile(portfolioInfosFile); if (portfolioInfos.getType() == Statement.Type.PortfolioInfos) { for (int i = 0, ei = portfolioInfos.size(); i < ei; i++) { Statement statement = portfolioInfos.get(i); String countryString = statement .getValueAsString(guiBundleWrapper.getString("PortfolioInfo_Country")); Country c; try { c = Country.valueOf(countryString); } catch (IllegalArgumentException ex) { log.error(null, ex); continue; } assert (c != null); if (c == country) { return c; } Double _portfolioSize = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioInfo_Size")); if (_portfolioSize == null) { continue; } int portfolioSize = (int) (double) _portfolioSize; if (portfolioSize > portfolioMaxSize) { portfolioMaxSize = portfolioSize; portfolioCountry = c; } } } Component selectedComponent = jTabbedPane1.getSelectedComponent(); if (selectedComponent == this.jPanel8) { // Watchlist if (watchlistCountry != null) { return watchlistCountry; } } else if (selectedComponent == this.portfolioManagementJPanel) { // Portfolio if (portfolioCountry != null) { return portfolioCountry; } } else { if (watchlistMaxSize > portfolioMaxSize) { if (watchlistCountry != null) { return watchlistCountry; } } else { if (portfolioCountry != null) { return portfolioCountry; } } } return country; } /* Reload after downloading from cloud. Take note that we must reload * JStockOptions before and outside this method, due to insensitive data * requirement. */ public void reloadAfterDownloadFromCloud(JStockOptions newJStockOptions) { final String oldLookNFeel = this.jStockOptions.getLooknFeel(); assert (newJStockOptions != null); this.jStockOptions.insensitiveCopy(newJStockOptions); final String newLookNFeel = this.jStockOptions.getLooknFeel(); if (oldLookNFeel != null) { if (false == oldLookNFeel.equals(newLookNFeel)) { this.setLookAndFeel(newLookNFeel); } } else { if (null != newLookNFeel) { this.setLookAndFeel(newLookNFeel); } } Utils.updateFactoriesPriceSource(); jStockOptions.setCountry(this.getBestCountryAfterDownloadFromCloud()); /* These codes are very similar to clean up code during changing country. */ JStock.this.statusBar.setCountryIcon(jStockOptions.getCountry().icon, jStockOptions.getCountry().humanString); // Here is the dirty trick here. We let our the 'child' panels perform // cleanup/ initialization first before initStockCodeAndSymbolDatabase. // This is because all child panels and stock symbol database task do // interact with status bar. However, We are only most interest in stock symbol // database, as it will be the most busy. Hence, we let the stock symbol // database to be the last, so that its interaction will overwrite the others. this.portfolioManagementJPanel.initPortfolio(); this.indicatorScannerJPanel.stop(); this.indicatorScannerJPanel.clear(); // Need to read user-defined-database.xml. // The user-defined-database.xml is extracted from cloud // freshly. this.initDatabase(true); this.initAjaxProvider(); this.initRealTimeIndexMonitor(); this.initMarketJPanel(); this.initStockHistoryMonitor(); this.initOthersStockHistoryMonitor(); this.initExchangeRateMonitor(); // Initialize real time monitor must come before initialize real time // stocks. We need to submit real time stocks to real time stock monitor. // Hence, after we load real time stocks from file, real time stock monitor // must be ready (initialized). this.initRealTimeStockMonitor(); this.initWatchlist(); this.initAlertStateManager(); this.initDynamicCharts(); this.initDynamicChartVisibility(); this.initAlwaysOnTop(); for (Enumeration<AbstractButton> e = this.buttonGroup2.getElements(); e.hasMoreElements();) { AbstractButton button = e.nextElement(); javax.swing.JRadioButtonMenuItem m = (javax.swing.JRadioButtonMenuItem) button; if (m.getText().equals(jStockOptions.getCountry().humanString)) { m.setSelected(true); break; } } if (null != this.indicatorPanel) { this.indicatorPanel.initIndicatorProjectManager(); this.indicatorPanel.initModuleProjectManager(); } // I will try to reload the GUI settings for Stock Watchlist and Stock // Indicator Scanner. I hope that the sudden change in GUI will not give // user a shock. this.initGUIOptions(); this.indicatorScannerJPanel.initGUIOptions(); } private void changeCountry(Country country) { if (country == null) { return; } if (jStockOptions.getCountry() == country) { return; } final Country oldCountry = jStockOptions.getCountry(); if (needToSaveUserDefinedDatabase) { // We are having updated user database in memory. // Save it to disk. this.saveUserDefinedDatabaseAsCSV(oldCountry, stockInfoDatabase); } /* Save the GUI look. */ saveGUIOptions(); /* Need to save chart dialog options? */ saveWatchlist(); this.portfolioManagementJPanel.savePortfolio(); // Spain no longer supported. Sad... if (country == Country.Spain) { JOptionPane.showMessageDialog(null, MessagesBundle.getString("info_message_spain_not_supported"), MessagesBundle.getString("info_title_spain_not_supported"), JOptionPane.INFORMATION_MESSAGE); } jStockOptions.setCountry(country); jStockOptions.addRecentCountry(country); JStock.this.statusBar.setCountryIcon(country.icon, country.humanString); // Here is the dirty trick here. We let our the 'child' panels perform // cleanup/ initialization first before initStockCodeAndSymbolDatabase. // This is because all child panels and stock symbol database task do // interact with status bar. However, We are only most interest in stock symbol // database, as it will be the most busy. Hence, we let the stock symbol // database to be the last, so that its interaction will overwrite the others. this.portfolioManagementJPanel.initPortfolio(); this.indicatorScannerJPanel.stop(); this.indicatorScannerJPanel.clear(); this.initDatabase(true); this.initAjaxProvider(); this.initRealTimeIndexMonitor(); this.initMarketJPanel(); this.initStockHistoryMonitor(); this.initOthersStockHistoryMonitor(); this.initExchangeRateMonitor(); // Initialize real time monitor must come before initialize real time // stocks. We need to submit real time stocks to real time stock monitor. // Hence, after we load real time stocks from file, real time stock monitor // must be ready (initialized). this.initRealTimeStockMonitor(); this.initWatchlist(); this.initAlertStateManager(); this.initDynamicCharts(); // this.initDynamicChartVisibility(); for (Enumeration<AbstractButton> e = this.buttonGroup2.getElements(); e.hasMoreElements();) { AbstractButton button = e.nextElement(); javax.swing.JRadioButtonMenuItem m = (javax.swing.JRadioButtonMenuItem) button; // Ugly fix on spelling mistake. if (country == Country.UnitedState && m.getText().equals(country.toString() + "s")) { m.setSelected(true); break; } if (m.getText().equals(country.toString())) { m.setSelected(true); break; } } } private MouseAdapter getMyJXStatusBarExchangeRateLabelMouseAdapter() { return new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { // Popup dialog to select currency exchange option. OptionsJDialog optionsJDialog = new OptionsJDialog(JStock.this, true); optionsJDialog.setLocationRelativeTo(JStock.this); optionsJDialog.set(jStockOptions); optionsJDialog.select(GUIBundle.getString("OptionsJPanel_Wealth")); optionsJDialog.setVisible(true); } } }; } private MouseAdapter getMyJXStatusBarCountryLabelMouseAdapter() { return new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { CountryJDialog countryJDialog = new CountryJDialog(JStock.this, true); countryJDialog.setLocationRelativeTo(JStock.this); countryJDialog.setCountry(jStockOptions.getCountry()); countryJDialog.setVisible(true); final Country country = countryJDialog.getCountry(); changeCountry(country); } } }; } private MouseAdapter getMyJXStatusBarImageLabelMouseAdapter() { return new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { // Make sure no other task is running. // Use local variable to be thread safe. final DatabaseTask task = JStock.this.databaseTask; if (task != null) { if (task.isDone() == true) { // Task is done. But, does it success? boolean success = false; // Some developers suggest that check for isCancelled before calling get // to avoid CancellationException. Others suggest that just perform catch // on all Exceptions. I will do it both. if (task.isCancelled() == false) { try { success = task.get(); } catch (InterruptedException ex) { log.error(null, ex); } catch (ExecutionException ex) { log.error(null, ex); } catch (CancellationException ex) { log.error(null, ex); } } if (success == false) { // Fail. Automatically reload database for user. Need not to prompt them message. // As, they do not have any database right now. JStock.this.initDatabase(true); } else { final int result = JOptionPane.showConfirmDialog(JStock.this, MessagesBundle.getString("question_message_perform_server_reconnecting"), MessagesBundle.getString("question_title_perform_server_reconnecting"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); if (result == JOptionPane.YES_OPTION) { JStock.this.initDatabase(false); } } } else { // There is task still running. Ask user whether he wants // to stop it. final int result = JOptionPane.showConfirmDialog(JStock.this, MessagesBundle.getString("question_message_cancel_server_reconnecting"), MessagesBundle.getString("question_title_cancel_server_reconnecting"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); if (result == JOptionPane.YES_OPTION) { synchronized (JStock.this.databaseTaskMonitor) { JStock.this.databaseTask.cancel(true); JStock.this.databaseTask = null; } setStatusBar(false, GUIBundle.getString("MainFrame_NetworkError")); statusBar.setImageIcon(getImageIcon("/images/16x16/network-error.png"), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_DoubleClickedToTryAgain")); } } } else { // User cancels databaseTask explicitly. (Cancel while // JStock is fetching database from server). Let's read // from disk. initDatabase(true); } } } }; } public StockInfoDatabase getStockInfoDatabase() { return this.stockInfoDatabase; } public StockNameDatabase getStockNameDatabase() { return stockNameDatabase; } public java.util.List<Stock> getStocks() { final StockTableModel tableModel = (StockTableModel) jTable1.getModel(); return tableModel.getStocks(); } // Should we synchronized the jTable1, or post the job to GUI event dispatch // queue? public void addStockToTable(final Stock stock, final StockAlert alert) { final JTable _jTable1 = this.jTable1; if (java.awt.EventQueue.isDispatchThread()) { final StockTableModel tableModel = (StockTableModel) _jTable1.getModel(); tableModel.addStock(stock, alert); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final StockTableModel tableModel = (StockTableModel) _jTable1.getModel(); tableModel.addStock(stock, alert); } }); } } public void addStockToTable(final Stock stock) { final JTable _jTable1 = this.jTable1; if (java.awt.EventQueue.isDispatchThread()) { final StockTableModel tableModel = (StockTableModel) _jTable1.getModel(); tableModel.addStock(stock); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final StockTableModel tableModel = (StockTableModel) _jTable1.getModel(); tableModel.addStock(stock); } }); } } // Only will return true if the selected stock is the one and only one. private boolean isStockBeingSelected(final Stock stock) { int[] rows = JStock.this.jTable1.getSelectedRows(); if (rows.length == 1) { final int row = rows[0]; final StockTableModel tableModel = (StockTableModel) jTable1.getModel(); final int modelIndex = jTable1.convertRowIndexToModel(row); if (stock.code.equals(tableModel.getStock(modelIndex).code)) { return true; } } return false; } // Return one and only one selected stock. Otherwise null. private Stock getSelectedStock() { int[] rows = JStock.this.jTable1.getSelectedRows(); if (rows.length == 1) { int row = rows[0]; StockTableModel tableModel = (StockTableModel) jTable1.getModel(); int modelIndex = jTable1.convertRowIndexToModel(row); return tableModel.getStock(modelIndex); } return null; } private void updateStockToTable(final Stock stock) { StockTableModel tableModel = (StockTableModel) jTable1.getModel(); tableModel.updateStock(stock); } private void update(final Indicator indicator, Boolean result) { final boolean flag = result; if (flag == false) { return; } final StockTableModel stockTableModel = (StockTableModel) JStock.this.jTable1.getModel(); final Stock stock = indicator.getStock(); final Double price = ((OperatorIndicator) indicator).getName().equalsIgnoreCase("fallbelow") ? stockTableModel.getFallBelow(stock) : stockTableModel.getRiseAbove(stock); final double lastPrice = stock.getLastPrice(); // Using lastPrice = 0 to compare against fall below and rise above // target price is meaningless. In normal condition, no stock price // shall fall until 0. When we get last price is 0, most probably // market is not opened yet. if (lastPrice <= 0.0) { return; } if (this.jStockOptions.isPopupMessage()) { final Runnable r = new Runnable() { @Override public void run() { String message = ""; if (((OperatorIndicator) indicator).getName().equalsIgnoreCase("fallbelow")) { final String template = GUIBundle.getString("MainFrame_FallBelow_template"); message = MessageFormat.format(template, stock.symbol, lastPrice, price); } else { final String template = GUIBundle.getString("MainFrame_RiseAbove_template"); message = MessageFormat.format(template, stock.symbol, lastPrice, price); } if (jStockOptions.isPopupMessage()) { displayPopupMessage(stock.symbol.toString(), message); 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.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 (this.jStockOptions.isSendEmail()) { final Runnable r = new Runnable() { @Override public void run() { String title = ""; if (((OperatorIndicator) indicator).getName().equalsIgnoreCase("fallbelow")) { final String template = GUIBundle.getString("MainFrame_FallBelow_template"); title = MessageFormat.format(template, stock.symbol, lastPrice, price); } else { final String template = GUIBundle.getString("MainFrame_RiseAbove_template"); title = MessageFormat.format(template, stock.symbol, lastPrice, price); } 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); } } /* if(jStockOptions.isSendEmail()) */ } /** * Highlight stock at row <code>modelRow</code>, by making it selected and * visible. * * @param modelRow row respected to stock model */ private void highlightStock(int modelRow) { if (modelRow < 0) { return; } final int row = this.jTable1.convertRowIndexToView(modelRow); // Make it selected. this.jTable1.getSelectionModel().setSelectionInterval(row, row); // and visible. JTableUtilities.scrollToVisible(this.jTable1, row, 0); } private org.yccheok.jstock.engine.Observer<AutoCompleteJComboBox, DispType> getDispObserver() { return new org.yccheok.jstock.engine.Observer<AutoCompleteJComboBox, DispType>() { @Override public void update(AutoCompleteJComboBox subject, DispType dispType) { assert (dispType != null); Code code = Code.newInstance(dispType.getDispCode()); final Symbol symbol = Symbol.newInstance(dispType.getDispName()); final StockInfo stockInfo = StockInfo.newInstance(code, symbol); addStockInfoFromAutoCompleteJComboBox(stockInfo); } }; } private org.yccheok.jstock.engine.Observer<AutoCompleteJComboBox, StockInfo> getStockInfoObserver() { return new org.yccheok.jstock.engine.Observer<AutoCompleteJComboBox, StockInfo>() { @Override public void update(AutoCompleteJComboBox subject, StockInfo stockInfo) { assert (stockInfo != null); addStockInfoFromAutoCompleteJComboBox(stockInfo); } // public void update(AutoCompleteJComboBox subject, StockInfo stockInfo) }; } // Shared code for getStockInfoObserver and getResultObserver. private void addStockInfoFromAutoCompleteJComboBox(StockInfo stockInfo) { // When user try to enter a stock, and the stock is already in // the table, the stock shall be highlighted. Stock will be // selected and the table shall be scrolled to be visible. final StockTableModel tableModel = (StockTableModel) JStock.this.jTable1.getModel(); final Stock emptyStock = org.yccheok.jstock.engine.Utils.getEmptyStock(stockInfo); // First add the empty stock, so that the user will not have wrong perspective that // our system is slow. addStockToTable(emptyStock); int row = tableModel.findRow(emptyStock); realTimeStockMonitor.addStockCode(stockInfo.code); realTimeStockMonitor.startNewThreadsIfNecessary(); realTimeStockMonitor.refresh(); JStock.this.highlightStock(row); } private org.yccheok.jstock.engine.Observer<Indicator, Boolean> getAlertStateManagerObserver() { return new org.yccheok.jstock.engine.Observer<Indicator, Boolean>() { @Override public void update(Indicator subject, Boolean arg) { JStock.this.update(subject, arg); } }; } // 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) { JStock.this.update(monitor, stocks); } }; } private org.yccheok.jstock.engine.Observer<RealTimeIndexMonitor, java.util.List<Market>> getRealTimeIndexMonitorObserver() { return new org.yccheok.jstock.engine.Observer<RealTimeIndexMonitor, java.util.List<Market>>() { @Override public void update(RealTimeIndexMonitor monitor, java.util.List<Market> markets) { JStock.this.update(markets); } }; } 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) { JStock.this.update(monitor, runnable); } }; } // Asynchronous call. Must be called by event dispatch thread. public void displayHistoryChart(Stock stock) { final StockHistoryServer stockHistoryServer = stockHistoryMonitor.getStockHistoryServer(stock.code); if (stockHistoryServer == null) { if (stockCodeHistoryGUI.add(stock.code) && stockHistoryMonitor.addStockCode(stock.code)) { final String template = GUIBundle.getString("MainFrame_LookingForHistory_template"); final String message = MessageFormat.format(template, stock.symbol, stockCodeHistoryGUI.size()); setStatusBar(true, message); } } else { ChartJDialog chartJDialog = new ChartJDialog(JStock.this, stock.symbol + " (" + stock.code + ")", false, stockHistoryServer); chartJDialog.setVisible(true); } } public void displayHistoryCharts() { int rows[] = jTable1.getSelectedRows(); final StockTableModel tableModel = (StockTableModel) jTable1.getModel(); for (int row : rows) { final int modelIndex = jTable1.getRowSorter().convertRowIndexToModel(row); Stock stock = tableModel.getStock(modelIndex); displayHistoryChart(stock); } } public void displayStockNews(Stock stock) { assert (SwingUtilities.isEventDispatchThread()); final StockInfo stockInfo = StockInfo.newInstance(stock.code, stock.symbol); final String title = stock.symbol + " (" + stock.code + ")"; final StockNewsJFrame stockNewsJFrame = new StockNewsJFrame(this, stockInfo, title); } private void displayStocksNews() { int rows[] = jTable1.getSelectedRows(); final StockTableModel tableModel = (StockTableModel) jTable1.getModel(); for (int row : rows) { final int modelIndex = jTable1.getRowSorter().convertRowIndexToModel(row); final Stock stock = tableModel.getStock(modelIndex); displayStockNews(stock); } } private JPopupMenu getMyJTablePopupMenu() { final JPopupMenu popup = new JPopupMenu(); final TableModel tableModel = jTable1.getModel(); javax.swing.JMenuItem menuItem = new JMenuItem( java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("MainFrame_History..."), this.getImageIcon("/images/16x16/strokedocker.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { displayHistoryCharts(); } }); popup.add(menuItem); menuItem = new JMenuItem( java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("MainFrame_News..."), this.getImageIcon("/images/16x16/news.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { displayStocksNews(); } }); popup.add(menuItem); popup.addSeparator(); if (jTable1.getSelectedRowCount() == 1) { menuItem = new JMenuItem( java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("MainFrame_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 Stock stock = ((StockTableModel) tableModel).getStock(modelIndex); // We have a real nasty bug here. We retrieve stock information through stock code. // When we receive stock information, we update all its particular information, including stock // symbol. Here is the catch, the latest updated stock symbol (stock.getSymbol), may not be the // same as stock symbol found in stock database. If we pass the stock symbol which is not found // in stock database to portfolio, something can go wrong. This is because portfolio rely heavily // on symbol <-> code conversion. Hence, instead of using stock.getSymbol, we prefer to get the // symbol out from stock database. This marks the close of the following reported bugs : // // [2800598] buyportfolio.xml file not updated with code symbol // [2790218] User unable to add new buy transaction in Spain // // Say no to : portfolioManagementJPanel.showNewBuyTransactionJDialog(stock.symbol, stock.getLastPrice(), false); portfolioManagementJPanel.showNewBuyTransactionJDialog(stock, stock.getLastPrice(), false); } }); popup.add(menuItem); popup.addSeparator(); } menuItem = new JMenuItem( java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui").getString("MainFrame_Delete"), this.getImageIcon("/images/16x16/editdelete.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { JStock.this.deteleSelectedTableRow(); } }); popup.add(menuItem); return popup; } private static boolean saveStockNameDatabaseAsCSV(Country country, StockNameDatabase stockNameDatabase) { final File stockNameDatabaseCSVFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database" + File.separator + "stock-name-database.csv"); final Statements statements = Statements.newInstanceFromStockNameDatabase(stockNameDatabase); boolean result = statements.saveAsCSVFile(stockNameDatabaseCSVFile); return result; } private static boolean saveStockInfoDatabaseAsCSV(Country country, StockInfoDatabase stockInfoDatabase) { org.yccheok.jstock.gui.Utils.createCompleteDirectoryHierarchyIfDoesNotExist( org.yccheok.jstock.engine.Utils.getStockInfoDatabaseFileDirectory(country)); final File stockInfoDatabaseCSVFile = org.yccheok.jstock.engine.Utils.getStockInfoDatabaseFile(country); final Statements statements = Statements.newInstanceFromStockInfoDatabase(stockInfoDatabase); boolean result = statements.saveAsCSVFile(stockInfoDatabaseCSVFile); return result; } private boolean saveUserDefinedDatabaseAsCSV(Country country, StockInfoDatabase stockInfoDatabase) { // Previously, we will store the entire stockcodeandsymboldatabase.xml // to cloud server if stockcodeandsymboldatabase.xml is containing // user defined code. Due to our server is running out of space, we will // only store UserDefined pair. user-defined-database.xml will be only // used for cloud storage purpose. final java.util.List<Pair<Code, Symbol>> pairs = getUserDefinedPair(stockInfoDatabase); // pairs can be empty. When it is empty, try to delete the file. // If deletion fail, we need to overwrite the file to reflect this. final File userDefinedDatabaseCSVFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database" + File.separator + "user-defined-database.csv"); if (pairs.isEmpty()) { if (userDefinedDatabaseCSVFile.delete() == true) { return true; } } final Statements statements = Statements.newInstanceFromUserDefinedDatabase(pairs); boolean result = statements.saveAsCSVFile(userDefinedDatabaseCSVFile); this.needToSaveUserDefinedDatabase = false; return result; } private java.util.List<Pair<Code, Symbol>> loadUserDefinedDatabaseFromCSV(Country country) { final File userDefinedDatabaseCSVFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database" + File.separator + "user-defined-database.csv"); Statements statements = Statements.newInstanceFromCSVFile(userDefinedDatabaseCSVFile); if (statements.getType() != Statement.Type.UserDefinedDatabase) { return new ArrayList<Pair<Code, Symbol>>(); } java.util.List<Pair<Code, Symbol>> pairs = new ArrayList<Pair<Code, Symbol>>(); for (int i = 0, ei = statements.size(); i < ei; i++) { Statement statement = statements.get(i); Atom atom0 = statement.getAtom(0); Atom atom1 = statement.getAtom(1); Code code = Code.newInstance(atom0.getValue().toString()); Symbol symbol = Symbol.newInstance(atom1.getValue().toString()); pairs.add(new Pair<Code, Symbol>(code, symbol)); } return pairs; } private StockNameDatabase loadStockNameDatabaseFromCSV(Country country) { final File stockNameDatabaseCSVFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database" + File.separator + "stock-name-database.csv"); Statements statements = Statements.newInstanceFromCSVFile(stockNameDatabaseCSVFile); if (statements.getType() != Statement.Type.StockNameDatabase) { return null; } java.util.List<Stock> stocks = new ArrayList<Stock>(); for (int i = 0, ei = statements.size(); i < ei; i++) { Statement statement = statements.get(i); Atom atom0 = statement.getAtom(0); Atom atom1 = statement.getAtom(1); Code code = Code.newInstance(atom0.getValue().toString()); String name = atom1.getValue().toString(); // Symbol doesn't matter. Just provide a dummy value for it. Stock stock = new Stock.Builder(code, Symbol.newInstance(code.toString())).name(name).build(); stocks.add(stock); } return new StockNameDatabase(stocks); } private StockInfoDatabase loadStockInfoDatabaseFromCSV(Country country) { final File stockInfoDatabaseCSVFile = org.yccheok.jstock.engine.Utils.getStockInfoDatabaseFile(country); Statements statements = Statements.newInstanceFromCSVFile(stockInfoDatabaseCSVFile); if (statements.getType() != Statement.Type.StockInfoDatabase) { return null; } java.util.List<Stock> stocks = new ArrayList<Stock>(); for (int i = 0, ei = statements.size(); i < ei; i++) { Statement statement = statements.get(i); Atom atom0 = statement.getAtom(0); Atom atom1 = statement.getAtom(1); Atom atom2 = statement.getAtom(2); Atom atom3 = statement.getAtom(3); Code code = Code.newInstance(atom0.getValue().toString()); Symbol symbol = Symbol.newInstance(atom1.getValue().toString()); Industry industry = Industry.Unknown; Board board = Board.Unknown; try { industry = Industry.valueOf(atom2.getValue().toString()); } catch (Exception exp) { log.error(null, exp); } try { board = Board.valueOf(atom3.getValue().toString()); } catch (Exception exp) { log.error(null, exp); } Stock stock = new Stock.Builder(code, symbol).board(board).industry(industry).build(); stocks.add(stock); } return new StockInfoDatabase(stocks); } // Task to initialize both stockInfoDatabase and stockNameDatabase. private class DatabaseTask extends SwingWorker<Boolean, Void> { private boolean readFromDisk = true; public DatabaseTask(boolean readFromDisk) { this.readFromDisk = readFromDisk; } @Override protected void done() { // The done Method: When you are informed that the SwingWorker // is done via a property change or via the SwingWorker object's // done method, you need to be aware that the get methods can // throw a CancellationException. A CancellationException is a // RuntimeException, which means you do not need to declare it // thrown and you do not need to catch it. Instead, you should // test the SwingWorker using the isCancelled method before you // use the get method. if (this.isCancelled()) { // Cancelled by user explicitly. Do not perform any GUI update. // No pop-up message. return; } boolean success = false; try { success = get(); } catch (InterruptedException exp) { log.error(null, exp); } catch (java.util.concurrent.ExecutionException exp) { log.error(null, exp); } catch (CancellationException ex) { // Not sure. Some developers suggest to catch this exception as // well instead of checking on isCancelled. I will do it both. log.error(null, ex); } if (success) { setStatusBar(false, getBestStatusBarMessage()); statusBar.setImageIcon(getImageIcon("/images/16x16/network-transmit-receive.png"), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_Connected")); } else { setStatusBar(false, GUIBundle.getString("MainFrame_NetworkError")); statusBar.setImageIcon(getImageIcon("/images/16x16/network-error.png"), java.util.ResourceBundle .getBundle("org/yccheok/jstock/data/gui").getString("MainFrame_DoubleClickedToTryAgain")); } } @Override public Boolean doInBackground() { final Country country = jStockOptions.getCountry(); Utils.createCompleteDirectoryHierarchyIfDoesNotExist( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database"); if (this.readFromDisk) { StockInfoDatabase tmp_stock_info_database = loadStockInfoDatabaseFromCSV(country); if (tmp_stock_info_database == null) { // Perhaps we are having a corrupted database. We will // restore from database.zip. initPreloadDatabase(true); tmp_stock_info_database = loadStockInfoDatabaseFromCSV(country); } // StockNameDatabase is an optional item. final StockNameDatabase tmp_name_database; if (org.yccheok.jstock.engine.Utils.isNameImmutable()) { tmp_name_database = JStock.this.loadStockNameDatabaseFromCSV(country); } else { tmp_name_database = null; } // After time consuming operation, check whether we should // cancel. if (this.isCancelled()) { return false; } if (tmp_stock_info_database != null && false == tmp_stock_info_database.isEmpty()) { // Yes. We need to integrate "user-defined-database.csv" into tmp_stock_info_database final java.util.List<Pair<Code, Symbol>> pairs = loadUserDefinedDatabaseFromCSV(country); boolean addUserDefinedStockInfoSuccessAtLeastOnce = false; if (pairs.isEmpty() == false) { // Remove the old user defined database. Legacy stockcodeandsymboldatabase.xml // may contain user defined codes. tmp_stock_info_database.removeAllUserDefinedStockInfos(); // Insert with new user defined code. for (Pair<Code, Symbol> pair : pairs) { if (tmp_stock_info_database .addUserDefinedStockInfo(StockInfo.newInstance(pair.first, pair.second))) { addUserDefinedStockInfoSuccessAtLeastOnce = true; } } } if (false == addUserDefinedStockInfoSuccessAtLeastOnce) { // user-defined-database.csv is no longer needed. new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database" + File.separator + "user-defined-database.csv").delete(); } // Prepare proper synchronization for us to change country. synchronized (JStock.this.databaseTaskMonitor) { if (this.isCancelled()) { return false; } JStock.this.stockInfoDatabase = tmp_stock_info_database; JStock.this.stockNameDatabase = tmp_name_database; // Register the auto complete JComboBox with latest database. ((AutoCompleteJComboBox) JStock.this.jComboBox1) .setStockInfoDatabase(JStock.this.stockInfoDatabase); JStock.this.indicatorPanel.setStockInfoDatabase(JStock.this.stockInfoDatabase); return true; } } // if (tmp_stock_info_database != null && false == tmp_stock_info_database.isEmpty()) } // if(this.readFromDisk) // When we fall here, we either fail to read from disk or user // explicitly doesn't allow us to read from disk. Let's perform // networking stuff. // // For networking stuff, we will try on JStock static server. final String location = org.yccheok.jstock.engine.Utils.getStocksCSVZipFileLocation(country); // Try to download the CSV file. final File zipFile = Utils.downloadAsTempFile(location); // Is download success? if (zipFile == null) { return false; } File tempZipDirectory = null; try { tempZipDirectory = java.nio.file.Files.createTempDirectory(null).toFile(); if (false == Utils.extractZipFile(zipFile, tempZipDirectory.getAbsolutePath(), true)) { return false; } File file = new File(tempZipDirectory, "stocks.csv"); // Try to parse the CSV file. final java.util.List<Stock> stocks = org.yccheok.jstock.engine.Utils.getStocksFromCSVFile(file); // Is the stocks good enough? if (false == stocks.isEmpty()) { final Pair<StockInfoDatabase, StockNameDatabase> stockDatabase = org.yccheok.jstock.engine.Utils .toStockDatabase(stocks, country); // After time consuming operation, check whether we should // cancel. if (this.isCancelled()) { return false; } // Save to disk. JStock.saveStockInfoDatabaseAsCSV(country, stockDatabase.first); if (stockDatabase.second != null) { JStock.saveStockNameDatabaseAsCSV(country, stockDatabase.second); } // Yes. We need to integrate "user-defined-database.csv" into tmp_stock_info_database final java.util.List<Pair<Code, Symbol>> pairs = loadUserDefinedDatabaseFromCSV(country); if (pairs.isEmpty() == false) { // Insert with new user defined code. for (Pair<Code, Symbol> pair : pairs) { stockDatabase.first .addUserDefinedStockInfo(StockInfo.newInstance(pair.first, pair.second)); } } // Prepare proper synchronization for us to change country. synchronized (JStock.this.databaseTaskMonitor) { if (this.isCancelled()) { return false; } JStock.this.stockInfoDatabase = stockDatabase.first; JStock.this.stockNameDatabase = stockDatabase.second; // Register the auto complete JComboBox with latest database. ((AutoCompleteJComboBox) JStock.this.jComboBox1) .setStockInfoDatabase(JStock.this.stockInfoDatabase); JStock.this.indicatorPanel.setStockInfoDatabase(JStock.this.stockInfoDatabase); return true; } } } catch (IOException ex) { log.error(null, ex); } finally { if (tempZipDirectory != null) { Utils.deleteDir(tempZipDirectory, true); } } return false; } } private void initMyJXStatusBarExchangeRateLabelMouseAdapter() { final MouseAdapter mouseAdapter = this.getMyJXStatusBarExchangeRateLabelMouseAdapter(); this.statusBar.addExchangeRateLabelMouseListener(mouseAdapter); } private void initMyJXStatusBarCountryLabelMouseAdapter() { final MouseAdapter mouseAdapter = this.getMyJXStatusBarCountryLabelMouseAdapter(); this.statusBar.addCountryLabelMouseListener(mouseAdapter); } private void initMyJXStatusBarImageLabelMouseAdapter() { final MouseAdapter mouseAdapter = this.getMyJXStatusBarImageLabelMouseAdapter(); this.statusBar.addImageLabelMouseListener(mouseAdapter); } /** * Initializes currency exchange monitor. */ public void initExchangeRateMonitor() { this.portfolioManagementJPanel.initExchangeRateMonitor(); } private void initRealTimeIndexMonitor() { final RealTimeIndexMonitor oldRealTimeIndexMonitor = realTimeIndexMonitor; if (oldRealTimeIndexMonitor != null) { zombiePool.execute(new Runnable() { @Override public void run() { log.info("Prepare to shut down " + oldRealTimeIndexMonitor + "..."); oldRealTimeIndexMonitor.clearIndices(); oldRealTimeIndexMonitor.dettachAll(); oldRealTimeIndexMonitor.stop(); log.info("Shut down " + oldRealTimeIndexMonitor + " peacefully."); } }); } realTimeIndexMonitor = new RealTimeIndexMonitor(Constants.REAL_TIME_INDEX_MONITOR_MAX_THREAD, Constants.REAL_TIME_INDEX_MONITOR_MAX_STOCK_SIZE_PER_SCAN, jStockOptions.getScanningSpeed()); realTimeIndexMonitor.attach(this.realTimeIndexMonitorObserver); for (Index index : org.yccheok.jstock.engine.Utils.getStockIndices(jStockOptions.getCountry())) { realTimeIndexMonitor.addIndex(index); } realTimeIndexMonitor.startNewThreadsIfNecessary(); } private void initRealTimeStockMonitor() { final RealTimeStockMonitor oldRealTimeStockMonitor = realTimeStockMonitor; if (oldRealTimeStockMonitor != null) { zombiePool.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."); } }); } realTimeStockMonitor = new RealTimeStockMonitor(Constants.REAL_TIME_STOCK_MONITOR_MAX_THREAD, Constants.REAL_TIME_STOCK_MONITOR_MAX_STOCK_SIZE_PER_SCAN, jStockOptions.getScanningSpeed()); realTimeStockMonitor.attach(this.realTimeStockMonitorObserver); this.indicatorScannerJPanel.initRealTimeStockMonitor(); this.portfolioManagementJPanel.initRealTimeStockMonitor(); } private void initGUIOptions() { final File f = new File( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "mainframe.xml"); GUIOptions guiOptions = 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)); } private void saveGUIOptions() { _saveGUIOptions(); this.indicatorScannerJPanel.saveGUIOptions(); this.portfolioManagementJPanel.saveGUIOptions(); } private 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 + "mainframe.xml"); return Utils.toXML(guiOptions, f); } /** * Initialize chart dialog options. */ private void initChartJDialogOptions() { final File f = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "chartjdialogoptions.xml"); final ChartJDialogOptions tmp = Utils.fromXML(ChartJDialogOptions.class, f); if (tmp == null) { this.chartJDialogOptions = new ChartJDialogOptions(); } else { this.chartJDialogOptions = tmp; log.info("chartJDialogOptions loaded from " + f.toString() + " successfully."); } } /** * Initialize JStock options. */ public void initJStockOptions(JStockOptions jStockOptions) { this.jStockOptions = jStockOptions; /* Hard core fix. */ if (this.jStockOptions.getScanningSpeed() == 0) { this.jStockOptions.setScanningSpeed(1 * 60 * 1000); } final String proxyHost = this.jStockOptions.getProxyServer(); final int proxyPort = this.jStockOptions.getProxyPort(); if ((proxyHost.length() > 0) && (org.yccheok.jstock.engine.Utils.isValidPortNumber(proxyPort))) { System.getProperties().put("http.proxyHost", proxyHost); System.getProperties().put("http.proxyPort", "" + proxyPort); } else { System.getProperties().remove("http.proxyHost"); System.getProperties().remove("http.proxyPort"); } Utils.updateFactoriesPriceSource(); } public void updatePriceSource(Country country, PriceSource priceSource) { Factories.INSTANCE.updatePriceSource(country, priceSource); rebuildRealTimeStockMonitor(); rebuildRealTimeIndexMonitor(); this.indicatorScannerJPanel.rebuildRealTimeStockMonitor(); this.portfolioManagementJPanel.rebuildRealTimeStockMonitor(); this.refreshAllRealTimeStockMonitors(); this.refreshRealTimeIndexMonitor(); this.refreshExchangeRateMonitor(); } private void rebuildRealTimeStockMonitor() { RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; if (_realTimeStockMonitor != null) { _realTimeStockMonitor.rebuild(); } } private void rebuildRealTimeIndexMonitor() { RealTimeIndexMonitor _realTimeIndexMonitor = this.realTimeIndexMonitor; if (_realTimeIndexMonitor != null) { _realTimeIndexMonitor.rebuild(); } } private void initWatchlist() { // This is new watchlist. Reset last update date. this.timestamp = 0; initCSVWatchlist(); } private boolean initCSVWatchlist() { java.util.List<String> availableWatchlistNames = org.yccheok.jstock.watchlist.Utils.getWatchlistNames(); // Do we have any watchlist for this country? if (availableWatchlistNames.size() <= 0) { // If not, create an empty watchlist. org.yccheok.jstock.watchlist.Utils .createEmptyWatchlist(org.yccheok.jstock.watchlist.Utils.getDefaultWatchlistName()); availableWatchlistNames = org.yccheok.jstock.watchlist.Utils.getWatchlistNames(); } assert (availableWatchlistNames.isEmpty() == false); // Is user selected watchlist name within current available watchlist names? if (false == availableWatchlistNames.contains(jStockOptions.getWatchlistName())) { // Nope. Reset user selected watchlist name to the first available name. jStockOptions.setWatchlistName(availableWatchlistNames.get(0)); } // openAsCSVFile will "append" stocks. We need to clear previous stocks // explicitly. // Clear the previous data structures. clearAllStocks(); File realTimeStockFile = org.yccheok.jstock.watchlist.Utils .getWatchlistFile(org.yccheok.jstock.watchlist.Utils.getWatchlistDirectory()); return this.openAsCSVFile(realTimeStockFile); } public static boolean saveCSVWatchlist(String directory, CSVWatchlist csvWatchlist) { assert (directory.endsWith(File.separator)); if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist(directory) == false) { return false; } return JStock.saveAsCSVFile(csvWatchlist, org.yccheok.jstock.watchlist.Utils.getWatchlistFile(directory), true); } private boolean saveCSVWathclist() { final String directory = org.yccheok.jstock.watchlist.Utils.getWatchlistDirectory(); final TableModel tableModel = jTable1.getModel(); CSVWatchlist csvWatchlist = CSVWatchlist.newInstance(tableModel); return JStock.saveCSVWatchlist(directory, csvWatchlist); } private boolean saveWatchlist() { return this.saveCSVWathclist(); } private boolean saveDatabase() { final Country country = jStockOptions.getCountry(); if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database") == false) { return false; } // Use local variable to ensure thread safety. final StockInfoDatabase stock_info_database = this.stockInfoDatabase; final StockNameDatabase name_database = this.stockNameDatabase; boolean b0 = true; if (name_database != null) { final Statements statements = Statements.newInstanceFromStockNameDatabase(name_database); final File stockNameDatabaseCSVFile = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + country + File.separator + "database" + File.separator + "stock-name-database.csv"); b0 = statements.saveAsCSVFile(stockNameDatabaseCSVFile); } if (stock_info_database == null) { return false; } // This could happen when OutOfMemoryException happen while fetching stock database information // from the server. if (stock_info_database.isEmpty()) { log.info("Database was corrupted."); return false; } final boolean b1 = saveUserDefinedDatabaseAsCSV(country, stock_info_database); // For optimization purpose. // symbol_database will always contain UserDefined code and non-UserDefined // code. As we may always recover UserDefined code from // user-defined-database.xml, we will not save the duplicated information. // // We will only do it, if stock-info-database.xml is not available, // which is very unlikely. Because during application startup, we will // always check the existance of stock-info-database.xml. boolean b2 = true; final File f = org.yccheok.jstock.engine.Utils.getStockInfoDatabaseFile(country); if (f.exists() == false) { b2 = saveStockInfoDatabaseAsCSV(country, stock_info_database); } return b0 && b1 && b2; } private static java.util.List<Pair<Code, Symbol>> getUserDefinedPair(StockInfoDatabase stockInfoDatabase) { java.util.List<Pair<Code, Symbol>> pairs = new ArrayList<Pair<Code, Symbol>>(); java.util.List<StockInfo> stockInfos = stockInfoDatabase.getUserDefinedStockInfos(); for (StockInfo stockInfo : stockInfos) { pairs.add(new Pair(stockInfo.code, stockInfo.symbol)); } return pairs; } /** * Save chart dialog options to disc. * @return <tt>true</tt> if saving operation is success */ private boolean saveChartJDialogOptions() { if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config") == false) { return false; } File f = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "chartjdialogoptions.xml"); return org.yccheok.jstock.gui.Utils.toXML(this.chartJDialogOptions, f); } /** * Save JStock options to disc. * @return <tt>true</tt> if saving operation is success */ private boolean saveJStockOptions() { if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config") == false) { return false; } File f = new File( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "options.xml"); return org.yccheok.jstock.gui.Utils.toXML(this.jStockOptions, f); } private void removeOldHistoryData(Country country) { // We do not want "yesterday" history record. We will remove 1 day old files. org.yccheok.jstock.gui.Utils.deleteAllOldFiles(new File(Utils.getHistoryDirectory(country)), 1); } private void initAlertStateManager() { alertStateManager.clearState(); alertStateManager.attach(alertStateManagerObserver); } private void initOthersStockHistoryMonitor() { this.indicatorPanel.initStockHistoryMonitor(); this.indicatorScannerJPanel.initStockHistoryMonitor(); } // Do not combine initOthersStockHistoryMonitor with initStockHistoryMonitor. We need to be able to update // only MainFrame's history monitor, when we change the history duration option. Other's history monitors // are not affected. private void initStockHistoryMonitor() { final StockHistoryMonitor oldStockHistoryMonitor = stockHistoryMonitor; if (oldStockHistoryMonitor != null) { zombiePool.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(this.stockHistoryMonitorObserver); final Country country = jStockOptions.getCountry(); removeOldHistoryData(country); StockHistorySerializer stockHistorySerializer = new StockHistorySerializer(Utils.getHistoryDirectory()); stockHistoryMonitor.setStockHistorySerializer(stockHistorySerializer); stockHistoryMonitor.setDuration(Duration.getTodayDurationByYears(jStockOptions.getHistoryDuration())); } public void initLatestNewsTask() { if (jStockOptions.isAutoUpdateNewsEnabled() == true) { if (latestNewsTask == null) { latestNewsTask = new LatestNewsTask(); latestNewsTask.execute(); } } else { final LatestNewsTask oldLatestNewsTask = latestNewsTask; if (oldLatestNewsTask != null) { zombiePool.execute(new Runnable() { @Override public void run() { log.info("Prepare to shut down " + oldLatestNewsTask + "..."); oldLatestNewsTask.cancel(true); log.info("Shut down " + oldLatestNewsTask + " peacefully."); } }); latestNewsTask = null; } } } private void initAjaxProvider() { Country country = this.jStockOptions.getCountry(); final AutoCompleteJComboBox autoCompleteJComboBox = ((AutoCompleteJComboBox) this.jComboBox1); if (country == Country.India) { autoCompleteJComboBox.setGreedyEnabled(true, Arrays.asList("N", "B")); } else { autoCompleteJComboBox.setGreedyEnabled(false, java.util.Collections.<String>emptyList()); } this.indicatorPanel.initAjaxProvider(); } private void initStockInfoDatabaseMeta() { Runnable runnable = new Runnable() { @Override public void run() { // Read existing stock-info-database-meta.json final Map<Country, Long> localStockInfoDatabaseMeta = Utils .loadStockInfoDatabaseMeta(Utils.getStockInfoDatabaseMetaFile()); final String location = org.yccheok.jstock.network.Utils .getURL(org.yccheok.jstock.network.Utils.Type.STOCK_INFO_DATABASE_META); final String json = Utils.downloadAsString(location); final Map<Country, Long> latestStockInfoDatabaseMeta = Utils.loadStockInfoDatabaseMeta(json); final Map<Country, Long> successStockInfoDatabaseMeta = new EnumMap<Country, Long>(Country.class); boolean needToInitDatabase = false; // Build up list of stock-info-database.csv that needed to be // updated. for (Map.Entry<Country, Long> entry : latestStockInfoDatabaseMeta.entrySet()) { if (Thread.currentThread().isInterrupted() || stockInfoDatabaseMetaPool == null) { break; } Country country = entry.getKey(); Long latest = entry.getValue(); Long local = localStockInfoDatabaseMeta.get(country); if (false == latest.equals(local)) { final String stocksCSVZipFileLocation = org.yccheok.jstock.engine.Utils .getStocksCSVZipFileLocation(country); final File zipFile = Utils.downloadAsTempFile(stocksCSVZipFileLocation); if (zipFile == null) { continue; } File tempZipDirectory = null; try { tempZipDirectory = java.nio.file.Files.createTempDirectory(null).toFile(); if (false == Utils.extractZipFile(zipFile, tempZipDirectory.getAbsolutePath(), true)) { continue; } File file = new File(tempZipDirectory, "stocks.csv"); final java.util.List<Stock> stocks = org.yccheok.jstock.engine.Utils .getStocksFromCSVFile(file); if (false == stocks.isEmpty()) { final Pair<StockInfoDatabase, StockNameDatabase> stockDatabase = org.yccheok.jstock.engine.Utils .toStockDatabase(stocks, country); final boolean success = JStock.saveStockInfoDatabaseAsCSV(country, stockDatabase.first); if (stockDatabase.second != null) { JStock.saveStockNameDatabaseAsCSV(country, stockDatabase.second); } if (success) { successStockInfoDatabaseMeta.put(country, latest); if (country == jStockOptions.getCountry()) { needToInitDatabase = true; } } } } catch (IOException ex) { log.error(null, ex); } finally { if (tempZipDirectory != null) { Utils.deleteDir(tempZipDirectory, true); } } } } if (successStockInfoDatabaseMeta.isEmpty()) { return; } // Retain old meta value. for (Map.Entry<Country, Long> entry : localStockInfoDatabaseMeta.entrySet()) { Country country = entry.getKey(); Long old = entry.getValue(); if (false == successStockInfoDatabaseMeta.containsKey(country)) { successStockInfoDatabaseMeta.put(country, old); } } Utils.saveStockInfoDatabaseMeta(Utils.getStockInfoDatabaseMetaFile(), successStockInfoDatabaseMeta); if (needToInitDatabase) { initDatabase(true); } } }; stockInfoDatabaseMetaPool.execute(runnable); } private void initDatabase(boolean readFromDisk) { // Update GUI state. this.setStatusBar(true, GUIBundle.getString("MainFrame_ConnectingToStockServerToRetrieveStockInformation...")); this.statusBar.setImageIcon(getImageIcon("/images/16x16/network-connecting.png"), java.util.ResourceBundle .getBundle("org/yccheok/jstock/data/gui").getString("MainFrame_Connecting...")); // Stop any on-going activities. // Entire block will be synchronized, as we do not want to hit by more // than 1 databaseTask running. synchronized (this.databaseTaskMonitor) { if (this.databaseTask != null) { this.databaseTask.cancel(true); this.stockInfoDatabase = null; this.stockNameDatabase = null; ((AutoCompleteJComboBox) this.jComboBox1).setStockInfoDatabase(null); this.indicatorPanel.setStockInfoDatabase(null); } this.databaseTask = new DatabaseTask(readFromDisk); this.databaseTask.execute(); } // We may hold a large database previously. Invoke garbage collector to perform cleanup. System.gc(); } private void update(RealTimeStockMonitor monitor, final java.util.List<Stock> stocks) { // We need to ignore symbol names given by stock server. Replace them // with database's. 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; // Sometimes server goes crazy by returning empty symbol. if (isSymbolImmutable || new_stock.symbol.toString().isEmpty()) { // Use local variable to ensure thread safety. final StockInfoDatabase stock_info_database = this.stockInfoDatabase; //final StockNameDatabase name_database = this.stockNameDatabase; 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."); } } else { // stockCodeAndSymbolDatabase is not ready yet. Use the information // from stock table. final StockTableModel tableModel = (StockTableModel) jTable1.getModel(); final int row = tableModel.findRow(stock); if (row >= 0) { final Symbol symbol = tableModel.getStock(row).symbol; new_stock = new_stock.deriveStock(symbol); } } // if (symbol_database != null) // Doesn't matter, as we do not need to show "name" in table. // Need not to perform derive for speed optimization. //if (org.yccheok.jstock.engine.Utils.isNameImmutable()) { // if (name_database != null) { // final String name = name_database.codeToName(stock.code); // if (name != null) { // new_stock = new_stock.deriveStock(name); // } else { // // Shouldn't be null. Let's give some warning on this. // log.error("Wrong stock code " + stock.code + " given by stock server."); // } // } else { // // stockNameDatabase is not ready yet. Use the information // // from stock table. // final StockTableModel tableModel = (StockTableModel)jTable1.getModel(); // final int row = tableModel.findRow(stock); // if (row >= 0) { // final String name = tableModel.getStock(row).getName(); // new_stock = new_stock.deriveStock(name); // } // } //} if (stock != new_stock) { stocks.set(i, new_stock); } } // if (isSymbolImmutable || new_stock.symbol.toString().isEmpty()) } // for (int i = 0, size = stocks.size(); i < size; i++) // Update status bar with current time string. this.timestamp = System.currentTimeMillis(); ((StockTableModel) jTable1.getModel()).setTimestamp(this.timestamp); JStock.instance().updateStatusBarWithLastUpdateDateMessageIfPossible(); // Do it in GUI event dispatch thread. Otherwise, we may face deadlock. // For example, we lock the jTable, and try to remove the stock from the // real time monitor. While we wait for the real time monitor to complete, // real time monitor will call this function and, be locked at function // updateStockToTable. javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { for (Stock stock : stocks) { updateStockToTable(stock); if (isStockBeingSelected(stock)) { JStock.this.updateDynamicChart(stock); } } } }); // Dynamic charting. Intraday trader might love this. for (Stock stock : stocks) { final Code code = stock.code; DynamicChart dynamicChart = this.dynamicCharts.get(code); if (dynamicChart == null) { // Not found. Try to create a new dynamic chart. if (this.dynamicCharts.size() <= JStock.MAX_DYNAMIC_CHART_SIZE) { dynamicChart = new DynamicChart(); this.dynamicCharts.put(code, dynamicChart); } else { // Full already. Shall we remove? if (this.isStockBeingSelected(stock)) { Set<Code> codes = this.dynamicCharts.keySet(); for (Code c : codes) { // Random remove. We do not care who is being removed. this.dynamicCharts.remove(c); if (this.dynamicCharts.size() <= JStock.MAX_DYNAMIC_CHART_SIZE) { // Remove success. break; } } dynamicChart = new DynamicChart(); this.dynamicCharts.put(code, dynamicChart); } } } /* if (dynamicChart == null) */ // Still null? if (dynamicChart == null) { // This usually indicate that dynamic chart list is full, and // no one is selecting this particular stock. continue; } if (this.isStockBeingSelected(stock)) { dynamicChart.addPriceObservation(stock.getTimestamp(), stock.getLastPrice()); final Stock s = stock; javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JStock.this.updateDynamicChart(s); } }); } else { // Although no one is watching at us, we still need to perform notification. // Weird? dynamicChart.addPriceObservation(stock.getTimestamp(), stock.getLastPrice()); } } /* for (Stock stock : stocks) */ // No alert is needed. Early return. if ((jStockOptions.isPopupMessage() == false) && (jStockOptions.isSoundEnabled() == false) && (jStockOptions.isSendEmail() == false)) { return; } final StockTableModel stockTableModel = (StockTableModel) jTable1.getModel(); for (Stock stock : stocks) { final Double fallBelow = stockTableModel.getFallBelow(stock); if (fallBelow != null) { final Indicator indicator = Utils.getLastPriceFallBelowIndicator(fallBelow); indicator.setStock(stock); alertStateManager.alert(indicator); } else { /* * Having FALL_BELOW_INDICATOR and RISE_ABOVE_INDICATOR, is to enable us * to remove a particular indicator from AlertStateManager, without the need * to call time-consuming getLastPriceFallBelowIndicator and * getLastPriceRiseAboveIndicator. In order for indicator to perform * correctly, we need to call indicator's mutable method (setStock). * However, since FALL_BELOW_INDICATOR and RISE_ABOVE_INDICATOR are * sharable variables, we are not allowed to call setStock outside * synchronized block. We need to perfrom a hacking liked workaround * Within syncrhonized block, call getStock (To get old stock), setStock and * restore back old stock. */ alertStateManager.clearState(FALL_BELOW_INDICATOR, stock); } final Double riseAbove = stockTableModel.getRiseAbove(stock); if (riseAbove != null) { final Indicator indicator = Utils.getLastPriceRiseAboveIndicator(riseAbove); indicator.setStock(stock); alertStateManager.alert(indicator); } else { /* * Having FALL_BELOW_INDICATOR and RISE_ABOVE_INDICATOR, is to enable us * to remove a particular indicator from AlertStateManager, without the need * to call time-consuming getLastPriceFallBelowIndicator and * getLastPriceRiseAboveIndicator. In order for indicator to perform * correctly, we need to call indicator's mutable method (setStock). * However, since FALL_BELOW_INDICATOR and RISE_ABOVE_INDICATOR are * sharable variables, we are not allowed to call setStock outside * synchronized block. We need to perfrom a hacking liked workaround * Within syncrhonized block, call getStock (To get old stock), setStock and * restore back old stock. */ alertStateManager.clearState(RISE_ABOVE_INDICATOR, stock); } } } public void updateStatusBarWithLastUpdateDateMessageIfPossible() { if (this.refreshPriceInProgress) { // Stop refresh price in progress message. this.setStatusBar(false, getBestStatusBarMessage()); this.refreshPriceInProgress = false; return; } if (this.isStatusBarBusy) { return; } if (this.getSelectedComponent() != this.jPanel8 && this.getSelectedComponent() != this.portfolioManagementJPanel) { return; } this.setStatusBar(false, getBestStatusBarMessage()); } // Connected // [My Watchlist] Last update: Connected // [My Watchlist] Last update: 10:40AM public String getBestStatusBarMessage() { final String currentName; final long _timestamp; // MainFrame if (this.getSelectedComponent() == this.jPanel8) { currentName = this.getJStockOptions().getWatchlistName(); _timestamp = this.timestamp; } else if (this.getSelectedComponent() == this.portfolioManagementJPanel) { currentName = this.getJStockOptions().getPortfolioName(); _timestamp = this.portfolioManagementJPanel.getTimestamp(); } else { return GUIBundle.getString("MainFrame_Connected"); } if (_timestamp == 0) { return MessageFormat.format(GUIBundle.getString("MainFrame_Connected_template"), currentName); } Date date = new Date(_timestamp); String time; if (Utils.isToday(_timestamp)) { time = Utils.getTodayLastUpdateTimeFormat().format(date); } else { time = Utils.getOtherDayLastUpdateTimeFormat().format(date); } return MessageFormat.format(GUIBundle.getString("MainFrame_LastUpdate_template"), currentName, time); } private void update(final java.util.List<Market> markets) { assert (markets.isEmpty() == false); javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { marketJPanel.update(markets); } }); } public void update(StockHistoryMonitor monitor, final StockHistoryMonitor.StockHistoryRunnable runnable) { javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Code code = runnable.getCode(); Symbol symbol = null; // Use local variable to ensure thread safety. final StockInfoDatabase stock_info_database = JStock.this.stockInfoDatabase; // Is the database ready? if (stock_info_database != null) { // Possible null if we are trying to get index history. symbol = stock_info_database.codeToSymbol(code); } final boolean shouldShowGUI = JStock.this.stockCodeHistoryGUI.remove(code); if (stockCodeHistoryGUI.isEmpty()) { if (runnable.getStockHistoryServer() != null) { final String template = GUIBundle.getString("MainFrame_HistorySuccess_template"); final String message = MessageFormat.format(template, (symbol != null ? symbol : code)); setStatusBar(false, message); } else { final String template = GUIBundle.getString("MainFrame_HistoryFailed_template"); final String message = MessageFormat.format(template, (symbol != null ? symbol : code)); setStatusBar(false, message); } } else { if (runnable.getStockHistoryServer() != null) { final String template = GUIBundle .getString("MainFrame_HistorySuccessStillWaitingForHistoryTotal_template"); final String message = MessageFormat.format(template, (symbol != null ? symbol : code), stockCodeHistoryGUI.size()); setStatusBar(true, message); } else { final String template = GUIBundle .getString("MainFrame_HistoryFailedStillWaitingForHistoryTotal_template"); final String message = MessageFormat.format(template, (symbol != null ? symbol : code), stockCodeHistoryGUI.size()); setStatusBar(true, message); } } if ((runnable.getStockHistoryServer() != null) && shouldShowGUI) { ChartJDialog chartJDialog = new ChartJDialog(JStock.this, (symbol != null ? symbol : code) + " (" + code + ")", false, runnable.getStockHistoryServer()); chartJDialog.setVisible(true); } } }); } private ImageIcon getImageIcon(String imageIcon) { return new javax.swing.ImageIcon(getClass().getResource(imageIcon)); } private class TableMouseAdapter extends MouseAdapter { @Override public void mouseClicked(MouseEvent evt) { int[] rows = JStock.this.jTable1.getSelectedRows(); if (rows.length == 1) { int row = rows[0]; StockTableModel tableModel = (StockTableModel) jTable1.getModel(); int modelIndex = jTable1.convertRowIndexToModel(row); Stock stock = tableModel.getStock(modelIndex); updateDynamicChart(stock); } else { updateDynamicChart(null); } if (evt.getClickCount() == 2) { // by definition of a dbl-click this will always only show one chart // because the dbl-click action cannot have multiple items selected displayHistoryCharts(); } } @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() <= 1) { setFocusToRightClickLocation(e, jTable1); } if (jTable1.getSelectedRowCount() > 0) { getMyJTablePopupMenu().show(e.getComponent(), e.getX(), e.getY()); } } } private void setFocusToRightClickLocation(MouseEvent e, JTable table) { // get the coordinates of the mouse click Point p = e.getPoint(); // get the row index that contains that coordinate int rowNumber = table.rowAtPoint(p); // either select the row or unselect row (if right-clicked outside rows if (rowNumber >= 0 && rowNumber < table.getRowCount()) { // set the selected interval of rows. Using the "rowNumber" // variable for the beginning and end selects only that one row. table.setRowSelectionInterval(rowNumber, rowNumber); } else { table.clearSelection(); } } } 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 java.util.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; } } } public void displayPopupMessage(final String caption, final String message) { if (trayIcon == null) { return; } if (SwingUtilities.isEventDispatchThread()) { trayIcon.displayMessage(caption, message, TrayIcon.MessageType.INFO); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { trayIcon.displayMessage(caption, message, TrayIcon.MessageType.INFO); } }); } } public IndicatorProjectManager getAlertIndicatorProjectManager() { return this.indicatorPanel.getAlertIndicatorProjectManager(); } public void updateScanningSpeed(int speed) { this.realTimeStockMonitor.setDelay(speed); this.realTimeIndexMonitor.setDelay(speed); this.indicatorScannerJPanel.updateScanningSpeed(speed); } public void updateHistoryDuration(Duration historyDuration) { final Duration oldDuration = stockHistoryMonitor.getDuration(); if (oldDuration.isContains(historyDuration)) { this.stockHistoryMonitor.setDuration(historyDuration); return; } // The history files that we are going to read, their duration are // too short compared to historyDuration. The easiest way to overcome // this problem is to remove them all. log.info("We are going to remove all history files, due to new duration " + historyDuration + " is not within old duration " + oldDuration); Country[] countries = Country.values(); for (Country country : countries) { Utils.deleteDir(Utils.getHistoryDirectory(country), false); } // Avoid from using old history monitor. History monitor contains their own memory data. // Since their duration are no longer valid, the memory data are no longer valid too. // this.initStockHistoryMonitor(); } public void repaintTable() { Component c = getSelectedComponent(); if (c instanceof IndicatorScannerJPanel) { indicatorScannerJPanel.repaintTable(); } else if (c instanceof IndicatorPanel) { } else { this.jTable1.repaint(); } } private void initMarketJPanel() { if (this.marketJPanel != null) { jPanel2.remove(marketJPanel); } this.marketJPanel = new MarketJPanel(jStockOptions.getCountry()); jPanel2.add(marketJPanel); jPanel2.revalidate(); } private void initPreloadDatabase(boolean overWrite) { /* No overwrite. */ Utils.extractZipFile("database" + File.separator + "database.zip", overWrite); } private class LatestNewsTask extends SwingWorker<Void, String> { // Delay first update checking for 20 seconds. private static final int SHORT_DELAY = 20 * 1000; private volatile CountDownLatch doneSignal; @Override protected void done() { } @Override protected void process(java.util.List<String> messages) { boolean show = false; for (String message : messages) { AutoUpdateNewsJDialog dialog = new AutoUpdateNewsJDialog(JStock.this, true); dialog.setNews(message); dialog.setVisible(true); show = true; } if (show) { doneSignal.countDown(); } } @Override protected Void doInBackground() { while (!isCancelled()) { try { Thread.sleep(SHORT_DELAY); } catch (InterruptedException ex) { log.info(null, ex); break; } final java.util.Map<String, String> map = Utils.getUUIDValue(org.yccheok.jstock.network.Utils .getURL(org.yccheok.jstock.network.Utils.Type.NEWS_INFORMATION_TXT)); final String newsID = JStock.this.getJStockOptions().getNewsID(); if (newsID.equals(map.get("news_id"))) { // Seen before. Quit. break; } final String location = map.get("news_url"); if (location == null) { break; } doneSignal = new CountDownLatch(1); final String respond = org.yccheok.jstock.gui.Utils .getResponseBodyAsStringBasedOnProxyAuthOption(location); if (respond == null) { break; } if (respond.indexOf(Utils.getJStockUUID()) < 0) { break; } publish(respond); try { doneSignal.await(); } catch (InterruptedException ex) { log.info(null, ex); break; } // Update jStockOptions, will make this while loop break // in next iteration. jStockOptions.setNewsID(map.get("news_id")); } return null; } } public Component getSelectedComponent() { return this.jTabbedPane1.getSelectedComponent(); } private void initAlwaysOnTop() { boolean selected = jStockOptions.isAlwaysOnTop(); this._setAlwaysOnTop(selected); } // Unlike a JButton, setIcon() does not add an icon to the text label. // Rather, in a radio button, the method is used to customize the icons used // to depict its state. However, by using the HTML capabilities in a label, // it is possible to add an icon to the label without affecting the // state-depicting icons. // We need to have image files being extracted outside executable jar file. private void initExtraDatas() { /* No overwrite. */ Utils.extractZipFile("extra" + File.separator + "extra.zip", false); } private ActionListener getTimerActionListener() { return new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (JStock.this.jTabbedPane1.getIconAt(4) == JStock.this.smileIcon) { JStock.this.jTabbedPane1.setIconAt(4, smileGrayIcon); } else { JStock.this.jTabbedPane1.setIconAt(4, smileIcon); } } }; } public void initDynamicChartVisibility() { jPanel10.setVisible(this.jStockOptions.isDynamicChartVisible()); } private void initDynamicCharts() { dynamicCharts.clear(); } private void initStatusBar() { final String message = java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_ConnectingToStockServerToRetrieveStockInformation..."); final ImageIcon icon = getImageIcon("/images/16x16/network-connecting.png"); final String iconMessage = java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui") .getString("MainFrame_Connecting..."); statusBar.setMainMessage(message).setImageIcon(icon, iconMessage) .setCountryIcon(jStockOptions.getCountry().icon, jStockOptions.getCountry().humanString); } private MouseAdapter getDynamicChartMouseAdapter() { return new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() >= 2) { final Stock stock = getSelectedStock(); if (stock == null) { return; } final DynamicChart dynamicChart = JStock.this.dynamicCharts.get(stock.code); if (dynamicChart == null) { return; } Symbol symbol = null; // Use local variable to ensure thread safety. final StockInfoDatabase stock_info_database = JStock.this.stockInfoDatabase; // Is the database ready? if (stock_info_database != null) { // Possible null if we are trying to get index history. symbol = stock_info_database.codeToSymbol(stock.code); } final String template = GUIBundle.getString("MainFrame_IntradayMovementTemplate"); final String message = MessageFormat.format(template, symbol == null ? stock.symbol : symbol); dynamicChart.showNewJDialog(JStock.this, message); } } // Shall we provide visualize mouse move over effect, so that user // knows this is a clickable component? /* private final LineBorder lineBorder = new LineBorder(Color.WHITE); private Border oldBorder = null; @Override public void mouseEntered(MouseEvent e) { JPanel jPanel = (JPanel)e.getComponent(); Border old = jPanel.getBorder(); if (old != lineBorder) { oldBorder = old; } jPanel.setBorder(lineBorder); } @Override public void mouseExited(MouseEvent e) { JPanel jPanel = (JPanel)e.getComponent(); jPanel.setBorder(oldBorder); } */ }; } private class TableKeyEventListener extends java.awt.event.KeyAdapter { @Override public void keyTyped(java.awt.event.KeyEvent e) { JStock.this.jTable1.getSelectionModel().clearSelection(); } } private void refreshExchangeRateMonitor() { this.portfolioManagementJPanel.refreshExchangeRateMonitor(); } public void refreshAllRealTimeStockMonitors() { RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; if (_realTimeStockMonitor != null) { _realTimeStockMonitor.refresh(); } this.indicatorScannerJPanel.refreshRealTimeStockMonitor(); this.portfolioManagementJPanel.refreshRealTimeStockMonitor(); } public void refreshRealTimeIndexMonitor() { RealTimeIndexMonitor _realTimeIndexMonitor = this.realTimeIndexMonitor; if (_realTimeIndexMonitor != null) { _realTimeIndexMonitor.refresh(); } } private TrayIcon trayIcon; private static final Log log = LogFactory.getLog(JStock.class); private final MyJXStatusBar statusBar = new MyJXStatusBar(); private boolean isStatusBarBusy = false; // A set of stock history which we need to display GUI on them, when user request explicitly. private final java.util.Set<Code> stockCodeHistoryGUI = new java.util.HashSet<>(); private volatile StockInfoDatabase stockInfoDatabase = null; // StockNameDatabase is an optional item. private volatile StockNameDatabase stockNameDatabase = null; private RealTimeStockMonitor realTimeStockMonitor = null; private RealTimeIndexMonitor realTimeIndexMonitor = null; private StockHistoryMonitor stockHistoryMonitor = null; private DatabaseTask databaseTask = null; private final Object databaseTaskMonitor = new Object(); private LatestNewsTask latestNewsTask = null; private JStockOptions jStockOptions; private ChartJDialogOptions chartJDialogOptions; private IndicatorPanel indicatorPanel; private IndicatorScannerJPanel indicatorScannerJPanel; private PortfolioManagementJPanel portfolioManagementJPanel; private final AlertStateManager alertStateManager = new AlertStateManager(); private final ExecutorService emailAlertPool = Executors.newFixedThreadPool(1); private final ExecutorService systemTrayAlertPool = Executors.newFixedThreadPool(1); private volatile ExecutorService stockInfoDatabaseMetaPool = Executors.newFixedThreadPool(1); private final org.yccheok.jstock.engine.Observer<RealTimeStockMonitor, java.util.List<Stock>> realTimeStockMonitorObserver = this .getRealTimeStockMonitorObserver(); private final org.yccheok.jstock.engine.Observer<RealTimeIndexMonitor, java.util.List<Market>> realTimeIndexMonitorObserver = this .getRealTimeIndexMonitorObserver(); private final org.yccheok.jstock.engine.Observer<StockHistoryMonitor, StockHistoryMonitor.StockHistoryRunnable> stockHistoryMonitorObserver = this .getStockHistoryMonitorObserver(); private final org.yccheok.jstock.engine.Observer<Indicator, Boolean> alertStateManagerObserver = this .getAlertStateManagerObserver(); private final javax.swing.ImageIcon smileIcon = this.getImageIcon("/images/16x16/smile.png"); private final javax.swing.ImageIcon smileGrayIcon = this.getImageIcon("/images/16x16/smile-gray.png"); private final Executor zombiePool = Utils.getZoombiePool(); private MarketJPanel marketJPanel; // Use ConcurrentHashMap, enable us able to read and write using different // threads. private final java.util.Map<Code, DynamicChart> dynamicCharts = new java.util.concurrent.ConcurrentHashMap<Code, DynamicChart>(); // We have 720 (6 * 60 * 2) points per chart, based on 10 seconds per points, with maximum 2 hours. // By having maximum 10 charts, we shall not face any memory problem. private static final int MAX_DYNAMIC_CHART_SIZE = 10; private static final DynamicChart EMPTY_DYNAMIC_CHART = new DynamicChart(); private final MouseAdapter dynamicChartMouseAdapter = getDynamicChartMouseAdapter(); private static final int HISTORY_MONITOR_MAX_THREAD = 4; /* * Having FALL_BELOW_INDICATOR and RISE_ABOVE_INDICATOR, is to enable us * to remove a particular indicator from AlertStateManager, without the need * to call time-consuming getLastPriceFallBelowIndicator and * getLastPriceRiseAboveIndicator. In order for indicator to perform * correctly, we need to call indicator's mutable method (setStock). * However, since FALL_BELOW_INDICATOR and RISE_ABOVE_INDICATOR are * sharable variables, we are not allowed to call setStock outside * synchronized block. We need to perfrom a hacking liked workaround * Within syncrhonized block, call getStock (To get old stock), setStock and * restore back old stock. */ private static final Indicator FALL_BELOW_INDICATOR = Utils.getLastPriceFallBelowIndicator(0.0); private static final Indicator RISE_ABOVE_INDICATOR = Utils.getLastPriceRiseAboveIndicator(0.0); // Do we need to save user defined database when we switch country or close // this application? private volatile boolean needToSaveUserDefinedDatabase = false; private volatile boolean isFormWindowClosedCalled = false; // The last time when we receive stock price update. private long timestamp = 0; private boolean refreshPriceInProgress = false; // To handle look n feel & always on top. private JMenuItem alwaysOnTopMenuItem = null; // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.ButtonGroup buttonGroup1; private javax.swing.ButtonGroup buttonGroup2; private javax.swing.ButtonGroup buttonGroup3; private javax.swing.ButtonGroup buttonGroup4; private javax.swing.JComboBox jComboBox1; private javax.swing.JLabel jLabel1; private javax.swing.JMenu jMenu1; private javax.swing.JMenu jMenu10; private javax.swing.JMenu jMenu11; private javax.swing.JMenu jMenu2; private javax.swing.JMenu jMenu3; private javax.swing.JMenu jMenu4; private javax.swing.JMenu jMenu5; private javax.swing.JMenu jMenu6; private javax.swing.JMenu jMenu7; private javax.swing.JMenu jMenu8; private javax.swing.JMenu jMenu9; private javax.swing.JMenuBar jMenuBar2; private javax.swing.JMenuItem jMenuItem1; private javax.swing.JMenuItem jMenuItem10; private javax.swing.JMenuItem jMenuItem11; private javax.swing.JMenuItem jMenuItem12; private javax.swing.JMenuItem jMenuItem13; private javax.swing.JMenuItem jMenuItem14; private javax.swing.JMenuItem jMenuItem15; private javax.swing.JMenuItem jMenuItem16; private javax.swing.JMenuItem jMenuItem17; private javax.swing.JMenuItem jMenuItem2; private javax.swing.JMenuItem jMenuItem3; private javax.swing.JMenuItem jMenuItem4; private javax.swing.JMenuItem jMenuItem5; private javax.swing.JMenuItem jMenuItem6; private javax.swing.JMenuItem jMenuItem7; private javax.swing.JMenuItem jMenuItem8; private javax.swing.JMenuItem jMenuItem9; private javax.swing.JPanel jPanel1; private javax.swing.JPanel jPanel10; private javax.swing.JPanel jPanel2; private javax.swing.JPanel jPanel3; private javax.swing.JPanel jPanel6; private javax.swing.JPanel jPanel8; private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem1; private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem2; private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem3; private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem4; private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem5; private javax.swing.JRadioButtonMenuItem jRadioButtonMenuItem6; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JPopupMenu.Separator jSeparator4; private javax.swing.JPopupMenu.Separator jSeparator5; private javax.swing.JPopupMenu.Separator jSeparator6; private javax.swing.JPopupMenu.Separator jSeparator7; private javax.swing.JPopupMenu.Separator jSeparator8; private javax.swing.JTabbedPane jTabbedPane1; private javax.swing.JTable jTable1; // End of variables declaration//GEN-END:variables }