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 java.awt.Color; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.text.DateFormat; import java.text.MessageFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.border.TitledBorder; import javax.swing.table.*; import javax.swing.tree.TreePath; import org.apache.commons.logging.*; import org.jdesktop.swingx.JXTableHeader; import org.jdesktop.swingx.JXTreeTable; import org.jdesktop.swingx.table.TableColumnExt; import org.jdesktop.swingx.treetable.*; import org.yccheok.jstock.engine.*; import org.yccheok.jstock.engine.currency.Currency; import org.yccheok.jstock.engine.currency.CurrencyPair; import org.yccheok.jstock.engine.currency.ExchangeRate; import org.yccheok.jstock.engine.currency.ExchangeRateMonitor; import org.yccheok.jstock.file.GUIBundleWrapper; import org.yccheok.jstock.file.Statement; import org.yccheok.jstock.file.Statements; import org.yccheok.jstock.gui.Utils.FileEx; import org.yccheok.jstock.gui.charting.InvestmentFlowChartJDialog; import org.yccheok.jstock.gui.portfolio.CommentJDialog; import org.yccheok.jstock.gui.portfolio.DepositSummaryJDialog; import org.yccheok.jstock.gui.portfolio.DepositSummaryTableModel; import org.yccheok.jstock.gui.portfolio.DividendSummaryBarChartJDialog; import org.yccheok.jstock.gui.portfolio.DividendSummaryJDialog; import org.yccheok.jstock.gui.portfolio.DividendSummaryTableModel; import org.yccheok.jstock.gui.portfolio.SplitJDialog; import org.yccheok.jstock.gui.portfolio.ToolTipHighlighter; import org.yccheok.jstock.gui.treetable.AbstractPortfolioTreeTableModelEx; import org.yccheok.jstock.gui.treetable.BuyPortfolioTreeTableModelEx; import org.yccheok.jstock.gui.treetable.DoubleWithCurrency; import org.yccheok.jstock.gui.treetable.SellPortfolioTreeTableModelEx; import org.yccheok.jstock.gui.treetable.SortableTreeTable; import org.yccheok.jstock.internationalization.GUIBundle; import org.yccheok.jstock.internationalization.MessagesBundle; import org.yccheok.jstock.portfolio.Commentable; import org.yccheok.jstock.portfolio.Contract; import org.yccheok.jstock.portfolio.DecimalPlace; import org.yccheok.jstock.portfolio.Deposit; import org.yccheok.jstock.portfolio.DepositSummary; import org.yccheok.jstock.portfolio.Dividend; import org.yccheok.jstock.portfolio.DividendSummary; import org.yccheok.jstock.portfolio.DoubleWrapper; import org.yccheok.jstock.portfolio.Portfolio; import org.yccheok.jstock.portfolio.PortfolioRealTimeInfo; import org.yccheok.jstock.portfolio.Transaction; import org.yccheok.jstock.portfolio.TransactionSummary; /** * * @author Owner */ public class PortfolioManagementJPanel extends javax.swing.JPanel { /** Creates new form PortfoliioJPanel */ public PortfolioManagementJPanel() { initComponents(); this.initPortfolio(); } /** This method is called from within the constructor to * initialize the form. * WARNING: Do NOT modify this code. The content of this method is * always regenerated by the Form Editor. */ // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { jPanel1 = new javax.swing.JPanel(); jPanel4 = new javax.swing.JPanel(); jPanel3 = new javax.swing.JPanel(); jLabel1 = new javax.swing.JLabel(); jLabel2 = new javax.swing.JLabel(); jLabel3 = new javax.swing.JLabel(); jLabel4 = new javax.swing.JLabel(); jPanel5 = new javax.swing.JPanel(); jLabel5 = new javax.swing.JLabel(); jLabel6 = new javax.swing.JLabel(); jLabel7 = new javax.swing.JLabel(); jLabel8 = new javax.swing.JLabel(); jSplitPane1 = new javax.swing.JSplitPane(); jScrollPane1 = new javax.swing.JScrollPane(); buyTreeTable = new SortableTreeTable(new BuyPortfolioTreeTableModelEx()); // We need to have a hack way, to have "Comment" in the model, but not visible to user. // So that our ToolTipHighlighter can work correctly. // setVisible should be called after JXTreeTable has been constructed. This is to avoid // initGUIOptions from calling JTable.removeColumn // ToolTipHighlighter will not work correctly if we tend to hide column view by removeColumn. // We need to hide the view by using TableColumnExt.setVisible. // Why? Don't ask me. Ask SwingX team. ((TableColumnExt) buyTreeTable.getColumn(GUIBundle.getString("PortfolioManagementJPanel_Comment"))) .setVisible(false); jScrollPane2 = new javax.swing.JScrollPane(); sellTreeTable = new SortableTreeTable(new SellPortfolioTreeTableModelEx()); // We need to have a hack way, to have "Comment" in the model, but not visible to user. // So that our ToolTipHighlighter can work correctly. // setVisible should be called after JXTreeTable has been constructed. This is to avoid // initGUIOptions from calling JTable.removeColumn // ToolTipHighlighter will not work correctly if we tend to hide column view by removeColumn. // We need to hide the view by using TableColumnExt.setVisible. // Why? Don't ask me. Ask SwingX team. ((TableColumnExt) sellTreeTable.getColumn(GUIBundle.getString("PortfolioManagementJPanel_Comment"))) .setVisible(false); jPanel2 = new javax.swing.JPanel(); jButton1 = new javax.swing.JButton(); jButton3 = new javax.swing.JButton(); jButton4 = new javax.swing.JButton(); jButton5 = new javax.swing.JButton(); addMouseListener(new java.awt.event.MouseAdapter() { public void mouseClicked(java.awt.event.MouseEvent evt) { formMouseClicked(evt); } }); setLayout(new java.awt.BorderLayout()); jPanel1.setBorder(javax.swing.BorderFactory.createTitledBorder("Portfolio Management")); jPanel1.setLayout(new java.awt.BorderLayout(0, 5)); jPanel4.setLayout(new java.awt.BorderLayout()); jPanel3.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT)); jLabel1.setText(getShareLabel()); jPanel3.add(jLabel1); jLabel2.setFont(jLabel2.getFont().deriveFont(jLabel2.getFont().getStyle() | java.awt.Font.BOLD)); jPanel3.add(jLabel2); jLabel3.setText(getCashLabel()); jPanel3.add(jLabel3); jLabel4.setFont(jLabel4.getFont().deriveFont(jLabel4.getFont().getStyle() | java.awt.Font.BOLD)); jPanel3.add(jLabel4); jPanel4.add(jPanel3, java.awt.BorderLayout.WEST); jLabel5.setText(getPaperProfitLabel()); jPanel5.add(jLabel5); jLabel6.setFont(jLabel6.getFont().deriveFont(jLabel6.getFont().getStyle() | java.awt.Font.BOLD)); jPanel5.add(jLabel6); jLabel7.setText(getRealizedProfitLabel()); jPanel5.add(jLabel7); jLabel8.setFont(jLabel8.getFont().deriveFont(jLabel8.getFont().getStyle() | java.awt.Font.BOLD)); jPanel5.add(jLabel8); jPanel4.add(jPanel5, java.awt.BorderLayout.EAST); jPanel1.add(jPanel4, java.awt.BorderLayout.NORTH); jSplitPane1.setDividerLocation(250); jSplitPane1.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/gui"); // NOI18N jScrollPane1.setBorder( javax.swing.BorderFactory.createTitledBorder(bundle.getString("PortfolioManagementJPanel_Buy"))); // NOI18N buyTreeTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_OFF); buyTreeTable.setRootVisible(true); // this must be before any sort instructions or get funny results buyTreeTable.setAutoCreateColumnsFromModel(false); buyTreeTable.addMouseListener(new BuyTableRowPopupListener()); buyTreeTable.addKeyListener(new TableKeyEventListener()); org.jdesktop.swingx.decorator.Highlighter highlighter0 = org.jdesktop.swingx.decorator.HighlighterFactory .createSimpleStriping(new Color(245, 245, 220)); buyTreeTable.addHighlighter(highlighter0); buyTreeTable.addHighlighter(new ToolTipHighlighter()); initTreeTableDefaultRenderer(buyTreeTable); // Not sure why. Without this code, sorting won't work just after you resize // table header. JTableHeader oldBuyTableHeader = buyTreeTable.getTableHeader(); JXTableHeader newBuyTableHeader = new JXTableHeader(oldBuyTableHeader.getColumnModel()); buyTreeTable.setTableHeader(newBuyTableHeader); // We need to have a hack way, to have "Comment" in the model, but not visible to user. // So that our ToolTipHighlighter can work correctly. buyTreeTable.getTableHeader().addMouseListener(new TableColumnSelectionPopupListener(1, new String[] { GUIBundle.getString("PortfolioManagementJPanel_Comment") })); buyTreeTable.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() { public void valueChanged(javax.swing.event.TreeSelectionEvent evt) { buyTreeTableValueChanged(evt); } }); jScrollPane1.setViewportView(buyTreeTable); jSplitPane1.setLeftComponent(jScrollPane1); jScrollPane2.setBorder( javax.swing.BorderFactory.createTitledBorder(bundle.getString("PortfolioManagementJPanel_Sell"))); // NOI18N sellTreeTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_OFF); sellTreeTable.setRootVisible(true); // this must be before any sort instructions or get funny results sellTreeTable.setAutoCreateColumnsFromModel(false); sellTreeTable.addMouseListener(new SellTableRowPopupListener()); sellTreeTable.addKeyListener(new TableKeyEventListener()); org.jdesktop.swingx.decorator.Highlighter highlighter1 = org.jdesktop.swingx.decorator.HighlighterFactory .createSimpleStriping(new Color(245, 245, 220)); sellTreeTable.addHighlighter(highlighter1); sellTreeTable.addHighlighter(new ToolTipHighlighter()); initTreeTableDefaultRenderer(sellTreeTable); // Not sure why. Without this code, sorting won't work just after you resize // table header. JTableHeader oldSellTableHeader = sellTreeTable.getTableHeader(); JXTableHeader newSellTableHeader = new JXTableHeader(oldSellTableHeader.getColumnModel()); sellTreeTable.setTableHeader(newSellTableHeader); // We need to have a hack way, to have "Comment" in the model, but not visible to user. // So that our ToolTipHighlighter can work correctly. sellTreeTable.getTableHeader().addMouseListener(new TableColumnSelectionPopupListener(1, new String[] { GUIBundle.getString("PortfolioManagementJPanel_Comment") })); sellTreeTable.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() { public void valueChanged(javax.swing.event.TreeSelectionEvent evt) { sellTreeTableValueChanged(evt); } }); jScrollPane2.setViewportView(sellTreeTable); jSplitPane1.setRightComponent(jScrollPane2); jPanel1.add(jSplitPane1, java.awt.BorderLayout.CENTER); add(jPanel1, java.awt.BorderLayout.CENTER); jButton1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/inbox.png"))); // NOI18N jButton1.setText(bundle.getString("PortfolioManagementJPanel_Buy...")); // NOI18N jButton1.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jButton1ActionPerformed(evt); } }); jPanel2.add(jButton1); jButton3.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/outbox.png"))); // NOI18N jButton3.setText(bundle.getString("PortfolioManagementJPanel_Sell...")); // NOI18N jButton3.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jButton3ActionPerformed(evt); } }); jPanel2.add(jButton3); jButton4.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/money.png"))); // NOI18N jButton4.setText(bundle.getString("PortfolioManagementJPanel_Cash...")); // NOI18N jButton4.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jButton4ActionPerformed(evt); } }); jPanel2.add(jButton4); jButton5.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/16x16/money2.png"))); // NOI18N jButton5.setText(bundle.getString("PortfolioManagementJPanel_Dividen...")); // NOI18N jButton5.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jButton5ActionPerformed(evt); } }); jPanel2.add(jButton5); add(jPanel2, java.awt.BorderLayout.SOUTH); }// </editor-fold>//GEN-END:initComponents private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed List<Stock> stocks = getSelectedStocks(); if (stocks.size() == 1) { this.showNewBuyTransactionJDialog(stocks.get(0), this.getStockPrice(stocks.get(0).code), true); } else { this.showNewBuyTransactionJDialog(null, 0.0, true); } }//GEN-LAST:event_jButton1ActionPerformed // Define our own renderers, so that we may have a consistent decimal places. private void initTreeTableDefaultRenderer(JXTreeTable treeTable) { final TableCellRenderer doubleOldTableCellRenderer = treeTable.getDefaultRenderer(Double.class); treeTable.setDefaultRenderer(Double.class, new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { final Component c = doubleOldTableCellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); final String UNITS = GUIBundle.getString("PortfolioManagementJPanel_Units"); // Do not manipulate display for "Units". We do not want 100 // "units" displayed as 100.00 "units". if (false == UNITS.equals(table.getColumnName(column))) { if (value != null) { if (c instanceof JLabel) { // Re-define the displayed value. // Ugly hacking. if (value instanceof DoubleWrapper) { DoubleWrapper v = (DoubleWrapper) value; ((JLabel) c).setText( org.yccheok.jstock.portfolio.Utils.toCurrency(v.decimalPlace, v.value)); } else if (value instanceof DoubleWithCurrency) { DoubleWithCurrency v = (DoubleWithCurrency) value; Currency currency = v.currency(); String content = org.yccheok.jstock.portfolio.Utils.toCurrency(v.decimalPlace(), v.Double()); if (currency == null) { ((JLabel) c).setText(content); } else { String prefix = currency.toString(); if (Currency.GBX.equals(prefix)) { // Special handling. prefix = Currency.GBP; } else if (Currency.ZAC.equals(prefix)) { // Special handling. prefix = Currency.ZAR; } ((JLabel) c).setText(prefix + " " + content); } } else { ((JLabel) c).setText( org.yccheok.jstock.portfolio.Utils.toCurrency(DecimalPlace.Four, value)); } } } } else { if (value != null) { if (c instanceof JLabel) { ((JLabel) c).setText(org.yccheok.jstock.portfolio.Utils.toUnits(value)); } } } return c; } }); treeTable.setDefaultRenderer(SimpleDate.class, new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { final Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (value != null) { if (c instanceof JLabel) { DateFormat dateFormat = DateFormat.getDateInstance(); SimpleDate simpleDate = (SimpleDate) value; ((JLabel) c).setText(dateFormat.format(simpleDate.getTime())); } } return c; } }); } private boolean isValidTreeTableNode(TreeTableModel treeTableModel, Object node) { boolean result = false; final Object root = treeTableModel.getRoot(); if (node instanceof TreeTableNode) { TreeTableNode ttn = (TreeTableNode) node; while (!result && ttn != null) { result = ttn == root; ttn = ttn.getParent(); } } return result; } private String getSelectedFirstColumnString(JXTreeTable treeTable) { final TreePath[] treePaths = treeTable.getTreeSelectionModel().getSelectionPaths(); if (treePaths == null) { return null; } if (treePaths.length == 1) { return treePaths[0].getLastPathComponent().toString(); } return null; } private Commentable getSelectedCommentable(JXTreeTable treeTable) { final TreePath[] treePaths = treeTable.getTreeSelectionModel().getSelectionPaths(); if (treePaths == null) { return null; } if (treePaths.length == 1) { if (treePaths[0].getLastPathComponent() instanceof Commentable) { return (Commentable) treePaths[0].getLastPathComponent(); } } return null; } public boolean openAsExcelFile(File file) { final java.util.List<Statements> statementsList = Statements.newInstanceFromExcelFile(file); boolean status = true; for (Statements statements : statementsList) { status = status & this.openAsStatements(statements, file); } return status; } public 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); if (statements.getType() == Statement.Type.PortfolioManagementBuy || statements.getType() == Statement.Type.PortfolioManagementSell || statements.getType() == Statement.Type.PortfolioManagementDeposit || statements.getType() == Statement.Type.PortfolioManagementDividend) { final GUIBundleWrapper guiBundleWrapper = statements.getGUIBundleWrapper(); // We will use a fixed date format (Locale.English), so that it will be // easier for Android to process. // // "Sep 5, 2011" - Locale.ENGLISH // "2011-9-5" - Locale.SIMPLIFIED_CHINESE // "2011/9/5" - Locale.TRADITIONAL_CHINESE // 05.09.2011 - Locale.GERMAN // // However, for backward compatible purpose (Able to read old CSV), // we perform a for loop to determine the best date format. DateFormat dateFormat = null; final int size = statements.size(); switch (statements.getType()) { case PortfolioManagementBuy: { final List<Transaction> transactions = new ArrayList<Transaction>(); for (int i = 0; i < size; i++) { final Statement statement = statements.get(i); final String _code = statement.getValueAsString(guiBundleWrapper.getString("MainFrame_Code")); final String _symbol = statement .getValueAsString(guiBundleWrapper.getString("MainFrame_Symbol")); final String _date = statement .getValueAsString(guiBundleWrapper.getString("PortfolioManagementJPanel_Date")); final Double units = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_Units")); final Double purchasePrice = statement.getValueAsDouble( guiBundleWrapper.getString("PortfolioManagementJPanel_PurchasePrice")); final Double broker = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_Broker")); final Double clearingFee = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_ClearingFee")); final Double stampDuty = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_StampDuty")); final String _comment = statement .getValueAsString(guiBundleWrapper.getString("PortfolioManagementJPanel_Comment")); Stock stock = null; if (_code.length() > 0 && _symbol.length() > 0) { stock = org.yccheok.jstock.engine.Utils.getEmptyStock(Code.newInstance(_code), Symbol.newInstance(_symbol)); } else { log.error("Unexpected empty stock. Ignore"); // stock is null. continue; } Date date = null; if (dateFormat == null) { // However, for backward compatible purpose (Able to read old CSV), // we perform a for loop to determine the best date format. // For the latest CSV, it should be Locale.ENGLISH. Locale[] locales = { Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE, Locale.GERMAN, Locale.TRADITIONAL_CHINESE, Locale.ITALIAN }; for (Locale locale : locales) { dateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); try { date = dateFormat.parse((String) _date); } catch (ParseException exp) { log.error(null, exp); date = null; dateFormat = null; continue; } // We had found our best dateFormat. Early break. break; } } else { // We already determine our best dateFormat. try { date = dateFormat.parse((String) _date); } catch (ParseException exp) { log.error(null, exp); } } if (date == null) { log.error("Unexpected wrong date. Ignore"); continue; } // Shall we continue to ignore, or shall we just return false to // flag an error? if (units == null) { log.error("Unexpected wrong units. Ignore"); continue; } if (purchasePrice == null || broker == null || clearingFee == null || stampDuty == null) { log.error("Unexpected wrong purchasePrice/broker/clearingFee/stampDuty. Ignore"); continue; } final SimpleDate simpleDate = new SimpleDate(date); final Contract.Type type = Contract.Type.Buy; final Contract.ContractBuilder builder = new Contract.ContractBuilder(stock, simpleDate); final Contract contract = builder.type(type).quantity(units).price(purchasePrice).build(); final Transaction t = new Transaction(contract, broker, stampDuty, clearingFee); t.setComment(org.yccheok.jstock.portfolio.Utils.replaceCSVLineFeedToSystemLineFeed(_comment)); transactions.add(t); } // We allow empty portfolio. //if (transactions.size() <= 0) { // return false; //} // Is there any exsiting displayed data? if (this.getBuyTransactionSize() > 0) { final String output = MessageFormat.format( MessagesBundle.getString("question_message_load_file_for_buy_portfolio_template"), file.getName()); final int result = javax.swing.JOptionPane.showConfirmDialog(JStock.instance(), output, MessagesBundle.getString("question_title_load_file_for_buy_portfolio"), javax.swing.JOptionPane.YES_NO_OPTION, javax.swing.JOptionPane.QUESTION_MESSAGE); if (result != javax.swing.JOptionPane.YES_OPTION) { // Assume success. return true; } } this.buyTreeTable.setTreeTableModel(new BuyPortfolioTreeTableModelEx()); final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); buyPortfolioTreeTableModel.bind(this.portfolioRealTimeInfo); buyPortfolioTreeTableModel.bind(this); Map<String, String> metadatas = statements.getMetadatas(); for (Transaction transaction : transactions) { final Code code = transaction.getStock().code; TransactionSummary transactionSummary = this.addBuyTransaction(transaction); if (transactionSummary != null) { String comment = metadatas.get(code.toString()); if (comment != null) { transactionSummary.setComment( org.yccheok.jstock.portfolio.Utils.replaceCSVLineFeedToSystemLineFeed(comment)); } } } // Only shows necessary columns. initGUIOptions(); expandTreeTable(buyTreeTable); updateRealTimeStockMonitorAccordingToPortfolioTreeTableModels(); updateExchangeRateMonitorAccordingToPortfolioTreeTableModels(); // updateWealthHeader will be called at end of switch. refreshStatusBarExchangeRateVisibility(); } break; case PortfolioManagementSell: { final List<Transaction> transactions = new ArrayList<Transaction>(); for (int i = 0; i < size; i++) { final Statement statement = statements.get(i); final String _code = statement.getValueAsString(guiBundleWrapper.getString("MainFrame_Code")); final String _symbol = statement .getValueAsString(guiBundleWrapper.getString("MainFrame_Symbol")); final String _referenceDate = statement.getValueAsString( guiBundleWrapper.getString("PortfolioManagementJPanel_ReferenceDate")); // Legacy file handling. PortfolioManagementJPanel_PurchaseBroker, PortfolioManagementJPanel_PurchaseClearingFee, // and PortfolioManagementJPanel_PurchaseStampDuty are introduced starting from 1.0.6x Double purchaseBroker = statement.getValueAsDouble( guiBundleWrapper.getString("PortfolioManagementJPanel_PurchaseBroker")); if (purchaseBroker == null) { // Legacy file handling. PortfolioManagementJPanel_PurchaseFee is introduced starting from 1.0.6s purchaseBroker = statement.getValueAsDouble( guiBundleWrapper.getString("PortfolioManagementJPanel_PurchaseFee")); if (purchaseBroker == null) { purchaseBroker = new Double(0.0); } } Double purchaseClearingFee = statement.getValueAsDouble( guiBundleWrapper.getString("PortfolioManagementJPanel_PurchaseClearingFee")); if (purchaseClearingFee == null) { purchaseClearingFee = new Double(0.0); } Double purchaseStampDuty = statement.getValueAsDouble( guiBundleWrapper.getString("PortfolioManagementJPanel_PurchaseStampDuty")); if (purchaseStampDuty == null) { purchaseStampDuty = new Double(0.0); } final String _date = statement .getValueAsString(guiBundleWrapper.getString("PortfolioManagementJPanel_Date")); final Double units = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_Units")); final Double sellingPrice = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_SellingPrice")); final Double purchasePrice = statement.getValueAsDouble( guiBundleWrapper.getString("PortfolioManagementJPanel_PurchasePrice")); final Double broker = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_Broker")); final Double clearingFee = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_ClearingFee")); final Double stampDuty = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_StampDuty")); final String _comment = statement .getValueAsString(guiBundleWrapper.getString("PortfolioManagementJPanel_Comment")); Stock stock = null; if (_code.length() > 0 && _symbol.length() > 0) { stock = org.yccheok.jstock.engine.Utils.getEmptyStock(Code.newInstance(_code), Symbol.newInstance(_symbol)); } else { log.error("Unexpected empty stock. Ignore"); // stock is null. continue; } Date date = null; Date referenceDate = null; if (dateFormat == null) { // However, for backward compatible purpose (Able to read old CSV), // we perform a for loop to determine the best date format. // For the latest CSV, it should be Locale.ENGLISH. Locale[] locales = { Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE, Locale.GERMAN, Locale.TRADITIONAL_CHINESE, Locale.ITALIAN }; for (Locale locale : locales) { dateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); try { date = dateFormat.parse((String) _date); referenceDate = dateFormat.parse((String) _referenceDate); } catch (ParseException exp) { log.error(null, exp); date = null; referenceDate = null; dateFormat = null; continue; } // We had found our best dateFormat. Early break. break; } } else { // We already determine our best dateFormat. try { date = dateFormat.parse((String) _date); referenceDate = dateFormat.parse((String) _referenceDate); } catch (ParseException exp) { log.error(null, exp); } } if (date == null || referenceDate == null) { log.error("Unexpected wrong date/referenceDate. Ignore"); continue; } // Shall we continue to ignore, or shall we just return false to // flag an error? if (units == null) { log.error("Unexpected wrong units. Ignore"); continue; } if (purchasePrice == null || broker == null || clearingFee == null || stampDuty == null || sellingPrice == null) { log.error( "Unexpected wrong purchasePrice/broker/clearingFee/stampDuty/sellingPrice. Ignore"); continue; } final SimpleDate simpleDate = new SimpleDate(date); final SimpleDate simpleReferenceDate = new SimpleDate(referenceDate); final Contract.Type type = Contract.Type.Sell; final Contract.ContractBuilder builder = new Contract.ContractBuilder(stock, simpleDate); final Contract contract = builder.type(type).quantity(units).price(sellingPrice) .referencePrice(purchasePrice).referenceDate(simpleReferenceDate) .referenceBroker(purchaseBroker).referenceClearingFee(purchaseClearingFee) .referenceStampDuty(purchaseStampDuty).build(); final Transaction t = new Transaction(contract, broker, stampDuty, clearingFee); t.setComment(org.yccheok.jstock.portfolio.Utils.replaceCSVLineFeedToSystemLineFeed(_comment)); transactions.add(t); } // for // We allow empty portfolio. //if (transactions.size() <= 0) { // return false; //} // Is there any exsiting displayed data? if (this.getSellTransactionSize() > 0) { final String output = MessageFormat.format( MessagesBundle.getString("question_message_load_file_for_sell_portfolio_template"), file.getName()); final int result = javax.swing.JOptionPane.showConfirmDialog(JStock.instance(), output, MessagesBundle.getString("question_title_load_file_for_sell_portfolio"), javax.swing.JOptionPane.YES_NO_OPTION, javax.swing.JOptionPane.QUESTION_MESSAGE); if (result != javax.swing.JOptionPane.YES_OPTION) { // Assume success. return true; } } this.sellTreeTable.setTreeTableModel(new SellPortfolioTreeTableModelEx()); final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); sellPortfolioTreeTableModel.bind(this.portfolioRealTimeInfo); sellPortfolioTreeTableModel.bind(this); Map<String, String> metadatas = statements.getMetadatas(); for (Transaction transaction : transactions) { final Code code = transaction.getStock().code; TransactionSummary transactionSummary = this.addSellTransaction(transaction); if (transactionSummary != null) { String comment = metadatas.get(code.toString()); if (comment != null) { transactionSummary.setComment( org.yccheok.jstock.portfolio.Utils.replaceCSVLineFeedToSystemLineFeed(comment)); } } } // Only shows necessary columns. initGUIOptions(); expandTreeTable(this.sellTreeTable); updateExchangeRateMonitorAccordingToPortfolioTreeTableModels(); // updateWealthHeader will be called at end of switch. refreshStatusBarExchangeRateVisibility(); } break; case PortfolioManagementDeposit: { final List<Deposit> deposits = new ArrayList<Deposit>(); for (int i = 0; i < size; i++) { Date date = null; final Statement statement = statements.get(i); final String object0 = statement .getValueAsString(guiBundleWrapper.getString("PortfolioManagementJPanel_Date")); assert (object0 != null); if (dateFormat == null) { // However, for backward compatible purpose (Able to read old CSV), // we will perform a for loop to determine the best date format. // For the latest CSV, it should be Locale.ENGLISH. Locale[] locales = { Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE, Locale.GERMAN, Locale.TRADITIONAL_CHINESE, Locale.ITALIAN }; for (Locale locale : locales) { dateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); try { date = dateFormat.parse(object0); } catch (ParseException exp) { log.error(null, exp); date = null; dateFormat = null; continue; } // We had found our best dateFormat. Early break. break; } } else { // We already determine our best dateFormat. try { date = dateFormat.parse(object0); } catch (ParseException exp) { log.error(null, exp); } } // Shall we continue to ignore, or shall we just return false to // flag an error? if (date == null) { log.error("Unexpected wrong date. Ignore"); continue; } final Double cash = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_Cash")); // Shall we continue to ignore, or shall we just return false to // flag an error? if (cash == null) { log.error("Unexpected wrong cash. Ignore"); continue; } final Deposit deposit = new Deposit(cash, new SimpleDate(date)); final String comment = statement .getValueAsString(guiBundleWrapper.getString("PortfolioManagementJPanel_Comment")); if (comment != null) { // Possible to be null. As in version <=1.0.6p, comment // is not being saved to CSV. deposit.setComment( org.yccheok.jstock.portfolio.Utils.replaceCSVLineFeedToSystemLineFeed(comment)); } deposits.add(deposit); } // We allow empty portfolio. //if (deposits.size() <= 0) { // return false; //} // Is there any exsiting displayed data? if (this.depositSummary.size() > 0) { final String output = MessageFormat.format( MessagesBundle.getString("question_message_load_file_for_cash_deposit_template"), file.getName()); final int result = javax.swing.JOptionPane.showConfirmDialog(JStock.instance(), output, MessagesBundle.getString("question_title_load_file_for_cash_deposit"), javax.swing.JOptionPane.YES_NO_OPTION, javax.swing.JOptionPane.QUESTION_MESSAGE); if (result != javax.swing.JOptionPane.YES_OPTION) { // Assume success. return true; } } this.depositSummary = new DepositSummary(); for (Deposit deposit : deposits) { depositSummary.add(deposit); } } break; case PortfolioManagementDividend: { final List<Dividend> dividends = new ArrayList<Dividend>(); for (int i = 0; i < size; i++) { Date date = null; StockInfo stockInfo = null; final Statement statement = statements.get(i); final String object0 = statement .getValueAsString(guiBundleWrapper.getString("PortfolioManagementJPanel_Date")); assert (object0 != null); if (dateFormat == null) { // However, for backward compatible purpose (Able to read old CSV), // we will perform a for loop to determine the best date format. // For the latest CSV, it should be Locale.ENGLISH. Locale[] locales = { Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE, Locale.GERMAN, Locale.TRADITIONAL_CHINESE, Locale.ITALIAN }; for (Locale locale : locales) { dateFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, locale); try { date = dateFormat.parse(object0); } catch (ParseException exp) { log.error(null, exp); date = null; dateFormat = null; continue; } // We had found our best dateFormat. Early break. break; } } else { // We already determine our best dateFormat. try { date = dateFormat.parse(object0); } catch (ParseException exp) { log.error(null, exp); } } // Shall we continue to ignore, or shall we just return false to // flag an error? if (date == null) { log.error("Unexpected wrong date. Ignore"); continue; } final Double dividend = statement .getValueAsDouble(guiBundleWrapper.getString("PortfolioManagementJPanel_Dividend")); // Shall we continue to ignore, or shall we just return false to // flag an error? if (dividend == null) { log.error("Unexpected wrong dividend. Ignore"); continue; } final String codeStr = statement.getValueAsString(guiBundleWrapper.getString("MainFrame_Code")); final String symbolStr = statement .getValueAsString(guiBundleWrapper.getString("MainFrame_Symbol")); if (codeStr.isEmpty() == false && symbolStr.isEmpty() == false) { stockInfo = StockInfo.newInstance(Code.newInstance(codeStr), Symbol.newInstance(symbolStr)); } else { log.error("Unexpected wrong stock. Ignore"); // stock is null. continue; } assert (stockInfo != null); assert (dividend != null); assert (date != null); final Dividend d = new Dividend(stockInfo, dividend, new SimpleDate(date)); final String comment = statement .getValueAsString(guiBundleWrapper.getString("PortfolioManagementJPanel_Comment")); if (comment != null) { // Possible to be null. As in version <=1.0.6p, comment // is not being saved to CSV. d.setComment( org.yccheok.jstock.portfolio.Utils.replaceCSVLineFeedToSystemLineFeed(comment)); } dividends.add(d); } // We allow empty portfolio. //if (dividends.size() <= 0) { // return false; //} if (this.dividendSummary.size() > 0) { final String output = MessageFormat.format( MessagesBundle.getString("question_message_load_file_for_dividend_template"), file.getName()); final int result = javax.swing.JOptionPane.showConfirmDialog(JStock.instance(), output, MessagesBundle.getString("question_title_load_file_for_dividend"), javax.swing.JOptionPane.YES_NO_OPTION, javax.swing.JOptionPane.QUESTION_MESSAGE); if (result != javax.swing.JOptionPane.YES_OPTION) { // Assume success. return true; } } this.dividendSummary = new DividendSummary(); for (Dividend dividend : dividends) { dividendSummary.add(dividend); } } break; default: assert (false); } // End of switch this.updateWealthHeader(); } else if (statements.getType() == Statement.Type.RealtimeInfo) { /* Open using other tabs. */ return JStock.instance().openAsStatements(statements, file); } else { return false; } return true; } private List<Stock> getSelectedStocks() { List<Stock> stocks0 = this.getSelectedStocks(buyTreeTable); List<Stock> stocks1 = this.getSelectedStocks(sellTreeTable); Set<Code> c = new HashSet<Code>(); List<Stock> stocks = new ArrayList<Stock>(); for (Stock stock : stocks0) { if (c.contains(stock.code) == false) { c.add(stock.code); stocks.add(stock); } } for (Stock stock : stocks1) { if (c.contains(stock.code) == false) { c.add(stock.code); stocks.add(stock); } } return Collections.unmodifiableList(stocks); } public double getStockPrice(Code code) { Double price = portfolioRealTimeInfo.stockPrices.get(code); if (price == null) { return 0.0; } return price; } private void showNewSellTransactionJDialog(List<Transaction> buyTransactions) { final JStock mainFrame = JStock.instance(); NewSellTransactionJDialog newSellTransactionJDialog = new NewSellTransactionJDialog(mainFrame, true); if (buyTransactions.size() > 1) { final String template = GUIBundle.getString("PortfolioManagementJPanel_BatchSell_template"); newSellTransactionJDialog.setTitle( MessageFormat.format(template, newSellTransactionJDialog.getTitle(), buyTransactions.size())); } newSellTransactionJDialog.setLocationRelativeTo(this); newSellTransactionJDialog.setBuyTransactions(buyTransactions); newSellTransactionJDialog.setVisible(true); final List<Transaction> newSellTransactions = newSellTransactionJDialog.getTransactions(); for (int i = 0; i < newSellTransactions.size(); i++) { Transaction newSellTransaction = newSellTransactions.get(i); Transaction buyTransaction = buyTransactions.get(i); final double remain = buyTransaction.getQuantity() - newSellTransaction.getQuantity(); assert (remain >= 0); addSellTransaction(newSellTransaction); final BuyPortfolioTreeTableModelEx portfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); if (remain <= 0) { portfolioTreeTableModel.removeTransaction(buyTransaction); } else { final double newBroker = buyTransaction.getBroker() - newSellTransaction.getReferenceBroker(); final double newStampDuty = buyTransaction.getStampDuty() - newSellTransaction.getReferenceStampDuty(); final double newClearingFee = buyTransaction.getClearingFee() - newSellTransaction.getReferenceClearingFee(); this.editBuyTransaction( buyTransaction.deriveWithQuantity(remain).deriveWithBroker(newBroker) .deriveWithStampDuty(newStampDuty).deriveWithClearingFee(newClearingFee), buyTransaction); } } updateWealthHeader(); } private void showEditTransactionJDialog(Transaction transaction) { final JStock mainFrame = JStock.instance(); if (transaction.getType() == Contract.Type.Buy) { NewBuyTransactionJDialog newTransactionJDialog = new NewBuyTransactionJDialog(mainFrame, true); newTransactionJDialog.setStockSelectionEnabled(false); newTransactionJDialog.setTransaction(transaction); final String template = GUIBundle.getString("PortfolioManagementJPanel_EditBuy_template"); newTransactionJDialog.setTitle(MessageFormat.format(template, transaction.getStock().symbol)); newTransactionJDialog.setLocationRelativeTo(this); newTransactionJDialog.setVisible(true); final Transaction newTransaction = newTransactionJDialog.getTransaction(); if (newTransaction != null) { this.editBuyTransaction(newTransaction, transaction); updateWealthHeader(); } } else { assert (transaction.getType() == Contract.Type.Sell); NewSellTransactionJDialog newTransactionJDialog = new NewSellTransactionJDialog(mainFrame, true); newTransactionJDialog.setSellTransaction(transaction); final String template = GUIBundle.getString("PortfolioManagementJPanel_EditSell_template"); newTransactionJDialog.setTitle(MessageFormat.format(template, transaction.getStock().symbol)); newTransactionJDialog.setLocationRelativeTo(this); newTransactionJDialog.setVisible(true); List<Transaction> transactions = newTransactionJDialog.getTransactions(); for (Transaction newTransaction : transactions) { this.editSellTransaction(newTransaction, transaction); updateWealthHeader(); } } } public void showNewBuyTransactionJDialog(Stock stock, double lastPrice, boolean JComboBoxEnabled) { final JStock mainFrame = JStock.instance(); final StockInfoDatabase stockInfoDatabase = mainFrame.getStockInfoDatabase(); if (stockInfoDatabase == null) { javax.swing.JOptionPane.showMessageDialog(this, MessagesBundle.getString("info_message_we_havent_connected_to_stock_server"), MessagesBundle.getString("info_title_we_havent_connected_to_stock_server"), javax.swing.JOptionPane.INFORMATION_MESSAGE); return; } NewBuyTransactionJDialog newTransactionJDialog = new NewBuyTransactionJDialog(mainFrame, true); newTransactionJDialog.setLocationRelativeTo(this); newTransactionJDialog.setStock(stock); newTransactionJDialog.setPrice(lastPrice); newTransactionJDialog.setJComboBoxEnabled(JComboBoxEnabled); newTransactionJDialog.setStockInfoDatabase(stockInfoDatabase); // If we are not in portfolio page, we shall provide user a hint, so that // user will know this transaction will go into which portfolio, without // having to click on the Portfolio drop-down menu. if (mainFrame.getSelectedComponent() != this) { final JStockOptions jStockOptions = mainFrame.getJStockOptions(); final String title = newTransactionJDialog.getTitle() + " (" + jStockOptions.getPortfolioName() + ")"; newTransactionJDialog.setTitle(title); } newTransactionJDialog.setVisible(true); final Transaction transaction = newTransactionJDialog.getTransaction(); if (transaction != null) { this.addBuyTransaction(transaction); this.refreshStatusBarExchangeRateVisibility(); this.updateWealthHeader(); } } public void clearTableSelection() { buyTreeTable.getSelectionModel().clearSelection(); sellTreeTable.getSelectionModel().clearSelection(); } private boolean deleteSelectedTreeTableRow(org.jdesktop.swingx.JXTreeTable treeTable) { final AbstractPortfolioTreeTableModelEx portfolioTreeTableModel = (AbstractPortfolioTreeTableModelEx) treeTable .getTreeTableModel(); final TreePath[] treePaths = treeTable.getTreeSelectionModel().getSelectionPaths(); if (treePaths == null) { return false; } boolean atLeastOnce = false; for (TreePath treePath : treePaths) { final Object o = treePath.getLastPathComponent(); if (portfolioTreeTableModel.getRoot() == o) { continue; } final MutableTreeTableNode mutableTreeTableNode = (MutableTreeTableNode) o; if (isValidTreeTableNode(portfolioTreeTableModel, mutableTreeTableNode) == false) { //??? portfolioTreeTableModel.fireTreeTableNodeChanged(mutableTreeTableNode); continue; } if (o instanceof Transaction) { portfolioTreeTableModel.removeTransaction((Transaction) o); atLeastOnce = true; } else if (o instanceof TransactionSummary) { portfolioTreeTableModel.removeTransactionSummary((TransactionSummary) o); atLeastOnce = true; } } return atLeastOnce; } private void deteleSelectedTreeTableRow() { boolean refreshStatusBarExchangeRateVisibility = false; if (deleteSelectedTreeTableRow(this.buyTreeTable)) { refreshStatusBarExchangeRateVisibility = true; } if (deleteSelectedTreeTableRow(this.sellTreeTable)) { refreshStatusBarExchangeRateVisibility = true; } ; if (refreshStatusBarExchangeRateVisibility) { this.refreshStatusBarExchangeRateVisibility(); } updateWealthHeader(); } private void buyTreeTableValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_buyTreeTableValueChanged javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (buyTreeTable.getSelectedRowCount() > 0) { sellTreeTable.clearSelection(); } } }); }//GEN-LAST:event_buyTreeTableValueChanged private void sellTreeTableValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_sellTreeTableValueChanged javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (sellTreeTable.getSelectedRowCount() > 0) { buyTreeTable.clearSelection(); } } }); }//GEN-LAST:event_sellTreeTableValueChanged private void formMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseClicked this.clearTableSelection(); }//GEN-LAST:event_formMouseClicked private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton3ActionPerformed final List<Stock> stocks = this.getSelectedStocks(buyTreeTable); if (stocks.size() != 1) { JOptionPane.showMessageDialog(this, java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages").getString( "info_message_you_need_to_select_only_single_stock_from_buy_portfolio_to_perform_sell_transaction"), java.util.ResourceBundle.getBundle("org/yccheok/jstock/data/messages").getString( "info_title_you_need_to_select_only_single_stock_from_buy_portfolio_to_perform_sell_transaction"), JOptionPane.INFORMATION_MESSAGE); return; } List<Transaction> transactions = this.getSelectedTransactions(buyTreeTable); this.showNewSellTransactionJDialog(transactions); }//GEN-LAST:event_jButton3ActionPerformed private void jButton4ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton4ActionPerformed showDepositSummaryJDialog(); }//GEN-LAST:event_jButton4ActionPerformed private void jButton5ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton5ActionPerformed showDividendSummaryJDialog(); }//GEN-LAST:event_jButton5ActionPerformed // When transaction summary being selected, we assume all its transactions are being selected. // This is most of the users intention too, I guess. private List<Transaction> getSelectedTransactions(JXTreeTable treeTable) { final TreePath[] treePaths = treeTable.getTreeSelectionModel().getSelectionPaths(); List<Transaction> transactions = new ArrayList<Transaction>(); if (treePaths == null) { return Collections.unmodifiableList(transactions); } for (TreePath treePath : treePaths) { final Object o = treePath.getLastPathComponent(); if (o instanceof Transaction) { final Transaction transaction = (Transaction) o; if (transactions.contains(transaction) == false) { transactions.add(transaction); } } else if (o instanceof TransactionSummary) { final TransactionSummary transactionSummary = (TransactionSummary) o; final int count = transactionSummary.getChildCount(); for (int i = 0; i < count; i++) { final Transaction transaction = (Transaction) transactionSummary.getChildAt(i); if (transactions.contains(transaction) == false) { transactions.add(transaction); } } } } return Collections.unmodifiableList(transactions); } private boolean isOnlyTreeTableRootBeingSelected(JXTreeTable treeTable) { if (treeTable.getSelectedRowCount() != 1) return false; final TreePath[] treePaths = treeTable.getTreeSelectionModel().getSelectionPaths(); final Object o = treePaths[0].getLastPathComponent(); final AbstractPortfolioTreeTableModelEx portfolioTreeTableModel = (AbstractPortfolioTreeTableModelEx) treeTable .getTreeTableModel(); return (portfolioTreeTableModel.getRoot() == o); } private void showDividendSummaryJDialog() { final JStock mainFrame = JStock.instance(); DividendSummaryJDialog dividendSummaryJDialog = new DividendSummaryJDialog(mainFrame, true, this.getDividendSummary(), this); dividendSummaryJDialog.setLocationRelativeTo(this); List<Stock> stocks = this.getSelectedStocks(); if (stocks.size() == 1) { dividendSummaryJDialog.addNewDividend(org.yccheok.jstock.engine.StockInfo.newInstance(stocks.get(0))); } dividendSummaryJDialog.setVisible(true); final DividendSummary _dividendSummary = dividendSummaryJDialog.getDividendSummaryAfterPressingOK(); if (_dividendSummary != null) { this.dividendSummary = _dividendSummary; updateWealthHeader(); } } private void showSplitOrMergeJDialog(StockInfo stockInfo) { final JStock mainFrame = JStock.instance(); SplitJDialog splitOrMergeJDialog = new SplitJDialog(mainFrame, true, stockInfo); splitOrMergeJDialog.pack(); splitOrMergeJDialog.setLocationRelativeTo(this); splitOrMergeJDialog.setVisible(true); if (splitOrMergeJDialog.getRatio() == null) { return; } double ratio = splitOrMergeJDialog.getRatio(); final BuyPortfolioTreeTableModelEx portfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); // Perform splitting. (Or merging) portfolioTreeTableModel.split(stockInfo, ratio); // Update the wealth. The value of wealth shall remain unchanged. this.updateWealthHeader(); } private void showDepositSummaryJDialog() { final JStock mainFrame = JStock.instance(); DepositSummaryJDialog depositSummaryJDialog = new DepositSummaryJDialog(mainFrame, true, this.getDepositSummary()); depositSummaryJDialog.setLocationRelativeTo(this); depositSummaryJDialog.setVisible(true); final DepositSummary _depositSummary = depositSummaryJDialog.getDepositSummaryAfterPressingOK(); if (_depositSummary != null) { this.depositSummary = _depositSummary; updateWealthHeader(); } } private void showCommentJDialog(Commentable commentable, String title) { if (commentable == null) { // Nothing to be shown. return; } final JStock mainFrame = JStock.instance(); CommentJDialog commentJDialog = new CommentJDialog(mainFrame, true, commentable); commentJDialog.setTitle(title); commentJDialog.setLocationRelativeTo(this); commentJDialog.setVisible(true); } /** * @return the depositSummary */ public DepositSummary getDepositSummary() { return depositSummary; } /** * @return the dividendSummary */ public DividendSummary getDividendSummary() { return dividendSummary; } private class TableKeyEventListener extends java.awt.event.KeyAdapter { @Override public void keyTyped(java.awt.event.KeyEvent e) { PortfolioManagementJPanel.this.clearTableSelection(); } } private class BuyTableRowPopupListener extends MouseAdapter { @Override public void mouseClicked(MouseEvent evt) { if (evt.getClickCount() == 2) { final List<Transaction> transactions = getSelectedTransactions(buyTreeTable); if (transactions.size() == 1) { PortfolioManagementJPanel.this.showEditTransactionJDialog(transactions.get(0)); } } } @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { getBuyTreeTablePopupMenu().show(e.getComponent(), e.getX(), e.getY()); } } } private class SellTableRowPopupListener extends MouseAdapter { @Override public void mouseClicked(MouseEvent evt) { if (evt.getClickCount() == 2) { final List<Transaction> transactions = getSelectedTransactions(sellTreeTable); if (transactions.size() == 1) { PortfolioManagementJPanel.this.showEditTransactionJDialog(transactions.get(0)); } } } @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { final JPopupMenu popupMenu = getSellTreeTablePopupMenu(); if (popupMenu != null) popupMenu.show(e.getComponent(), e.getX(), e.getY()); } } } private ImageIcon getImageIcon(String imageIcon) { return new javax.swing.ImageIcon(getClass().getResource(imageIcon)); } private void showBuyPortfolioChartJDialog() { final JStock m = JStock.instance(); final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); BuyPortfolioChartJDialog buyPortfolioChartJDialog = new BuyPortfolioChartJDialog(m, false, buyPortfolioTreeTableModel, this.portfolioRealTimeInfo, this.getDividendSummary()); buyPortfolioChartJDialog.setVisible(true); } private void showChashFlowChartJDialog() { final JStock m = JStock.instance(); InvestmentFlowChartJDialog cashFlowChartJDialog = new InvestmentFlowChartJDialog(m, false, this); cashFlowChartJDialog.setVisible(true); } private void showSellPortfolioChartJDialog() { final JStock m = JStock.instance(); final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); SellPortfolioChartJDialog sellPortfolioChartJDialog = new SellPortfolioChartJDialog(m, false, sellPortfolioTreeTableModel, this.portfolioRealTimeInfo, this.getDividendSummary()); sellPortfolioChartJDialog.setVisible(true); } private JPopupMenu getSellTreeTablePopupMenu() { final List<Transaction> transactions = getSelectedTransactions(this.sellTreeTable); JPopupMenu popup = new JPopupMenu(); JMenuItem menuItem = null; menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagementJPanel_Cash..."), this.getImageIcon("/images/16x16/money.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { showDepositSummaryJDialog(); } }); popup.add(menuItem); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Dividend..."), this.getImageIcon("/images/16x16/money2.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { showDividendSummaryJDialog(); } }); popup.add(menuItem); popup.addSeparator(); if (transactions.size() == 1) { menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Edit..."), this.getImageIcon("/images/16x16/edit.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.showEditTransactionJDialog(transactions.get(0)); } }); popup.add(menuItem); } final Commentable commentable = getSelectedCommentable(this.sellTreeTable); final String tmp = getSelectedFirstColumnString(this.sellTreeTable); if (commentable != null && tmp != null) { menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Note..."), this.getImageIcon("/images/16x16/sticky.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { final String template = GUIBundle.getString("PortfolioManagementJPanel_NoteFor_template"); final String title = MessageFormat.format(template, tmp); PortfolioManagementJPanel.this.showCommentJDialog(commentable, title); } }); popup.add(menuItem); popup.addSeparator(); } else if (transactions.size() == 1) { popup.addSeparator(); } menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_InvestmentChart..."), this.getImageIcon("/images/16x16/graph.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { showChashFlowChartJDialog(); } }); popup.add(menuItem); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagementJPanel_DividendChart"), this.getImageIcon("/images/16x16/chart.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.showDividendSummaryBarChartJDialog(); } }); popup.add(menuItem); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Summary..."), this.getImageIcon("/images/16x16/pie_chart.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.showSellPortfolioChartJDialog(); } }); popup.add(menuItem); if (isOnlyTreeTableRootBeingSelected(sellTreeTable) == false && (sellTreeTable.getSelectedRow() > 0)) { final JStock m = JStock.instance(); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_History..."), this.getImageIcon("/images/16x16/strokedocker.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { List<Stock> stocks = getSelectedStocks(sellTreeTable); for (Stock stock : stocks) { m.displayHistoryChart(stock); } } }); popup.addSeparator(); popup.add(menuItem); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_News..."), this.getImageIcon("/images/16x16/news.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { List<Stock> stocks = getSelectedStocks(sellTreeTable); for (Stock stock : stocks) { m.displayStockNews(stock); } } }); popup.add(menuItem); popup.addSeparator(); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Delete"), this.getImageIcon("/images/16x16/editdelete.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.deteleSelectedTreeTableRow(); } }); popup.add(menuItem); } return popup; } private void showDividendSummaryBarChartJDialog() { final JStock m = JStock.instance(); final DividendSummaryBarChartJDialog dividendSummaryBarChartJDialog = new DividendSummaryBarChartJDialog(m, false, this.getDividendSummary()); dividendSummaryBarChartJDialog.setVisible(true); } private JPopupMenu getBuyTreeTablePopupMenu() { JPopupMenu popup = new JPopupMenu(); JMenuItem menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Buy..."), this.getImageIcon("/images/16x16/inbox.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { List<Stock> stocks = getSelectedStocks(); if (stocks.size() == 1) { PortfolioManagementJPanel.this.showNewBuyTransactionJDialog(stocks.get(0), PortfolioManagementJPanel.this.getStockPrice(stocks.get(0).code), true); } else { PortfolioManagementJPanel.this.showNewBuyTransactionJDialog(org.yccheok.jstock.engine.Utils .getEmptyStock(Code.newInstance(""), Symbol.newInstance("")), 0.0, true); } } }); popup.add(menuItem); final List<Transaction> transactions = getSelectedTransactions(this.buyTreeTable); final List<Stock> stocks = this.getSelectedStocks(this.buyTreeTable); if (transactions.size() > 0 && stocks.size() == 1) { menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Sell..."), this.getImageIcon("/images/16x16/outbox.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.showNewSellTransactionJDialog(transactions); } }); popup.add(menuItem); } popup.addSeparator(); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagementJPanel_Cash..."), this.getImageIcon("/images/16x16/money.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { showDepositSummaryJDialog(); } }); popup.add(menuItem); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Dividend..."), this.getImageIcon("/images/16x16/money2.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { showDividendSummaryJDialog(); } }); popup.add(menuItem); popup.addSeparator(); boolean needToAddSeperator = false; if (transactions.size() == 1) { menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Edit..."), this.getImageIcon("/images/16x16/edit.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.showEditTransactionJDialog(transactions.get(0)); } }); popup.add(menuItem); needToAddSeperator = true; } final Commentable commentable = getSelectedCommentable(this.buyTreeTable); final String tmp = getSelectedFirstColumnString(this.buyTreeTable); if (commentable != null && tmp != null) { menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Note..."), this.getImageIcon("/images/16x16/sticky.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { final String template = GUIBundle.getString("PortfolioManagementJPanel_NoteFor_template"); final String title = MessageFormat.format(template, tmp); PortfolioManagementJPanel.this.showCommentJDialog(commentable, title); } }); popup.add(menuItem); needToAddSeperator = true; } // Split or merge only allowed, if there is one and only one stock // being selected. final List<Stock> selectedStocks = this.getSelectedStocks(); if (selectedStocks.size() == 1) { menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagementJPanel_SplitOrMerge"), this.getImageIcon("/images/16x16/merge.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this .showSplitOrMergeJDialog(StockInfo.newInstance(selectedStocks.get(0))); } }); popup.add(menuItem); needToAddSeperator = true; } if (needToAddSeperator) { popup.addSeparator(); } menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_InvestmentChart..."), this.getImageIcon("/images/16x16/graph.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { showChashFlowChartJDialog(); } }); popup.add(menuItem); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagementJPanel_DividendChart"), this.getImageIcon("/images/16x16/chart.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.showDividendSummaryBarChartJDialog(); } }); popup.add(menuItem); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Summary..."), this.getImageIcon("/images/16x16/pie_chart.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.showBuyPortfolioChartJDialog(); } }); popup.add(menuItem); if (isOnlyTreeTableRootBeingSelected(buyTreeTable) == false && (buyTreeTable.getSelectedRow() > 0)) { final JStock m = JStock.instance(); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_History..."), this.getImageIcon("/images/16x16/strokedocker.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { List<Stock> stocks = getSelectedStocks(buyTreeTable); for (Stock stock : stocks) { m.displayHistoryChart(stock); } } }); popup.addSeparator(); popup.add(menuItem); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_News..."), this.getImageIcon("/images/16x16/news.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { List<Stock> stocks = getSelectedStocks(buyTreeTable); for (Stock stock : stocks) { m.displayStockNews(stock); } } }); popup.add(menuItem); popup.addSeparator(); menuItem = new JMenuItem(GUIBundle.getString("PortfolioManagement_Delete"), this.getImageIcon("/images/16x16/editdelete.png")); menuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { PortfolioManagementJPanel.this.deteleSelectedTreeTableRow(); } }); popup.add(menuItem); } return popup; } private void editSellTransaction(Transaction newTransaction, Transaction oldTransaction) { assert (newTransaction.getType() == Contract.Type.Sell); assert (oldTransaction.getType() == Contract.Type.Sell); final SellPortfolioTreeTableModelEx portfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); portfolioTreeTableModel.editTransaction(newTransaction, oldTransaction); } private void editBuyTransaction(Transaction newTransaction, Transaction oldTransaction) { assert (newTransaction.getType() == Contract.Type.Buy); assert (oldTransaction.getType() == Contract.Type.Buy); final BuyPortfolioTreeTableModelEx portfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); portfolioTreeTableModel.editTransaction(newTransaction, oldTransaction); } private Set<Code> getCodes(org.yccheok.jstock.gui.treetable.SortableTreeTable treeTable) { Set<Code> codes = new HashSet<>(); final AbstractPortfolioTreeTableModelEx portfolioTreeTableModelEx = (AbstractPortfolioTreeTableModelEx) treeTable .getTreeTableModel(); Portfolio portfolio = (Portfolio) portfolioTreeTableModelEx.getRoot(); Enumeration<? extends MutableTreeTableNode> transactionSummaries = portfolio.children(); while (transactionSummaries.hasMoreElements()) { TransactionSummary transactionSummary = (TransactionSummary) transactionSummaries.nextElement(); Enumeration<? extends MutableTreeTableNode> transactions = transactionSummary.children(); if (transactions.hasMoreElements()) { Transaction transaction = (Transaction) transactionSummary.children().nextElement(); Stock stock = transaction.getStock(); codes.add(stock.code); } } return codes; } private Set<Code> getBuyCodes() { return getCodes(buyTreeTable); } private Set<Code> getSellCodes() { return getCodes(sellTreeTable); } private Set<Code> getCodes() { Set<Code> codes = getBuyCodes(); codes.addAll(getSellCodes()); return codes; } private Set<CurrencyPair> getCurrencyPairs() { final Currency localCurrency = org.yccheok.jstock.portfolio.Utils.getLocalCurrency(); if (localCurrency == null) { return Collections.<CurrencyPair>emptySet(); } Set<CurrencyPair> currencyPairs = new HashSet<>(); Set<Code> codes = getCodes(); for (Code code : codes) { final Currency stockCurrency = org.yccheok.jstock.portfolio.Utils .getStockCurrency(portfolioRealTimeInfo, code); if (stockCurrency.equals(localCurrency)) { continue; } CurrencyPair currencyPair = CurrencyPair.create(stockCurrency, localCurrency); currencyPairs.add(currencyPair); } return currencyPairs; } private int getBuyTransactionSize() { final BuyPortfolioTreeTableModelEx portfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); return portfolioTreeTableModel.getTransactionSize(); } private int getSellTransactionSize() { final SellPortfolioTreeTableModelEx portfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); return portfolioTreeTableModel.getTransactionSize(); } private TransactionSummary addBuyTransaction(Transaction transaction) { assert (transaction.getType() == Contract.Type.Buy); final BuyPortfolioTreeTableModelEx portfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); TransactionSummary transactionSummary = portfolioTreeTableModel.addTransaction(transaction); // This is to prevent NPE, during initPortfolio through constructor. // Information will be pumped in later to realTimeStockMonitor, through // initRealTimeStockMonitor. final RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; if (_realTimeStockMonitor != null) { _realTimeStockMonitor.addStockCode(transaction.getStock().code); _realTimeStockMonitor.startNewThreadsIfNecessary(); _realTimeStockMonitor.refresh(); } final ExchangeRateMonitor _exchangeRateMonitor = this.exchangeRateMonitor; if (_exchangeRateMonitor != null) { final Currency localCurrency = org.yccheok.jstock.portfolio.Utils.getLocalCurrency(); if (localCurrency != null) { final Currency stockCurrency = org.yccheok.jstock.portfolio.Utils .getStockCurrency(portfolioRealTimeInfo, transaction.getStock().code); if (false == stockCurrency.equals(localCurrency)) { _exchangeRateMonitor.addCurrencyPair(CurrencyPair.create(stockCurrency, localCurrency)); _exchangeRateMonitor.startNewThreadsIfNecessary(); _exchangeRateMonitor.refresh(); } } } return transactionSummary; } public List<TransactionSummary> getTransactionSummariesFromPortfolios() { final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); final Portfolio buyPortfolio = (Portfolio) buyPortfolioTreeTableModel.getRoot(); final Portfolio sellPortfolio = (Portfolio) sellPortfolioTreeTableModel.getRoot(); List<TransactionSummary> summaries = new ArrayList<>(); for (int i = 0, count = buyPortfolio.getChildCount(); i < count; i++) { summaries.add((TransactionSummary) buyPortfolio.getChildAt(i)); } for (int i = 0, count = sellPortfolio.getChildCount(); i < count; i++) { summaries.add((TransactionSummary) sellPortfolio.getChildAt(i)); } return summaries; } public List<StockInfo> getStockInfosFromPortfolios() { final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); final Portfolio buyPortfolio = (Portfolio) buyPortfolioTreeTableModel.getRoot(); final Portfolio sellPortfolio = (Portfolio) sellPortfolioTreeTableModel.getRoot(); Set<Code> codes = new HashSet<Code>(); List<StockInfo> stockInfos = new ArrayList<StockInfo>(); final int count = buyPortfolio.getChildCount(); TransactionSummary transactionSummary = null; for (int i = 0; i < count; i++) { transactionSummary = (TransactionSummary) buyPortfolio.getChildAt(i); assert (transactionSummary.getChildCount() > 0); Transaction transaction = (Transaction) transactionSummary.getChildAt(0); Stock stock = transaction.getStock(); if (codes.contains(stock.code) == false) { codes.add(stock.code); stockInfos.add(StockInfo.newInstance(stock)); } } final int count2 = sellPortfolio.getChildCount(); transactionSummary = null; for (int i = 0; i < count2; i++) { transactionSummary = (TransactionSummary) sellPortfolio.getChildAt(i); assert (transactionSummary.getChildCount() > 0); Transaction transaction = (Transaction) transactionSummary.getChildAt(0); Stock stock = transaction.getStock(); if (codes.contains(stock.code) == false) { codes.add(stock.code); stockInfos.add(StockInfo.newInstance(stock)); } } return stockInfos; } private TransactionSummary addSellTransaction(Transaction transaction) { assert (transaction.getType() == Contract.Type.Sell); final SellPortfolioTreeTableModelEx portfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); final TransactionSummary transactionSummary = portfolioTreeTableModel.addTransaction(transaction); final ExchangeRateMonitor _exchangeRateMonitor = this.exchangeRateMonitor; if (_exchangeRateMonitor != null) { final Currency localCurrency = org.yccheok.jstock.portfolio.Utils.getLocalCurrency(); if (localCurrency != null) { final Currency stockCurrency = org.yccheok.jstock.portfolio.Utils .getStockCurrency(portfolioRealTimeInfo, transaction.getStock().code); if (false == stockCurrency.equals(localCurrency)) { _exchangeRateMonitor.addCurrencyPair(CurrencyPair.create(stockCurrency, localCurrency)); _exchangeRateMonitor.startNewThreadsIfNecessary(); _exchangeRateMonitor.refresh(); } } } return transactionSummary; } private void updateExchangeRateMonitorAccordingToPortfolioTreeTableModels() { ExchangeRateMonitor _exchangeRateMonitor = this.exchangeRateMonitor; if (_exchangeRateMonitor == null) { return; } _exchangeRateMonitor.clearCurrencyPairs(); Set<CurrencyPair> currencyPairs = this.getCurrencyPairs(); for (CurrencyPair currencyPair : currencyPairs) { _exchangeRateMonitor.addCurrencyPair(currencyPair); } _exchangeRateMonitor.startNewThreadsIfNecessary(); _exchangeRateMonitor.refresh(); } private void updateRealTimeStockMonitorAccordingToPortfolioTreeTableModels() { RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; if (_realTimeStockMonitor == null) { return; } _realTimeStockMonitor.clearStockCodes(); for (Code code : getBuyCodes()) { _realTimeStockMonitor.addStockCode(code); } for (Code code : getSellCodes()) { // This is for optimization purpose. If we already have currency information for a stock // code, we will not further query for it. However, this does have its own shortcoming. // If the stock currency has been changed, we will not have chance to make any amendment. // So far, we treat this as an extremely rare case, and should not happen in real life. // However, if it does so, please remove the following optimization - contains(code) // check. if (false == this.portfolioRealTimeInfo.currencies.containsKey(code)) { _realTimeStockMonitor.addStockCode(code); } } _realTimeStockMonitor.startNewThreadsIfNecessary(); _realTimeStockMonitor.refresh(); } private List<Stock> getSelectedStocks(JXTreeTable treeTable) { final TreePath[] treePaths = treeTable.getTreeSelectionModel().getSelectionPaths(); List<Stock> stocks = new ArrayList<Stock>(); Set<Code> c = new HashSet<Code>(); if (treePaths == null) { return Collections.unmodifiableList(stocks); } for (TreePath treePath : treePaths) { final Object lastPathComponent = treePath.getLastPathComponent(); if (lastPathComponent instanceof TransactionSummary) { final TransactionSummary transactionSummary = (TransactionSummary) lastPathComponent; assert (transactionSummary.getChildCount() > 0); final Transaction transaction = (Transaction) transactionSummary.getChildAt(0); final Stock stock = transaction.getStock(); final Code code = stock.code; if (c.contains(code)) continue; stocks.add(stock); c.add(code); } else if (lastPathComponent instanceof Transaction) { final Transaction transaction = (Transaction) lastPathComponent; final Stock stock = transaction.getStock(); final Code code = stock.code; if (c.contains(code)) continue; stocks.add(stock); c.add(code); } } return Collections.unmodifiableList(stocks); } // Initialize portfolios through CSV files. This is the preferable method, // as it works well under Desktop platform and Android platform. private boolean initCSVPortfolio() { final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); List<String> availablePortfolioNames = org.yccheok.jstock.portfolio.Utils.getPortfolioNames(); // Do we have any portfolio for this country? if (availablePortfolioNames.size() <= 0) { // If not, create an empty portfolio. org.yccheok.jstock.portfolio.Utils .createEmptyPortfolio(org.yccheok.jstock.portfolio.Utils.getDefaultPortfolioName()); availablePortfolioNames = org.yccheok.jstock.portfolio.Utils.getPortfolioNames(); } assert (availablePortfolioNames.isEmpty() == false); // Is user selected portfolio name within current available portfolio names? if (false == availablePortfolioNames.contains(jStockOptions.getPortfolioName())) { // Nope. Reset user selected portfolio name to the first available name. jStockOptions.setPortfolioName(availablePortfolioNames.get(0)); } // Clear the previous data structures. PortfolioManagementJPanel.this.buyTreeTable.setTreeTableModel(new BuyPortfolioTreeTableModelEx()); PortfolioManagementJPanel.this.sellTreeTable.setTreeTableModel(new SellPortfolioTreeTableModelEx()); PortfolioManagementJPanel.this.depositSummary = new DepositSummary(); PortfolioManagementJPanel.this.dividendSummary = new DividendSummary(); final File buyPortfolioFile = new File( org.yccheok.jstock.portfolio.Utils.getPortfolioDirectory() + "buyportfolio.csv"); final File sellPortfolioFile = new File( org.yccheok.jstock.portfolio.Utils.getPortfolioDirectory() + "sellportfolio.csv"); final File depositSummaryFile = new File( org.yccheok.jstock.portfolio.Utils.getPortfolioDirectory() + "depositsummary.csv"); final File dividendSummaryFile = new File( org.yccheok.jstock.portfolio.Utils.getPortfolioDirectory() + "dividendsummary.csv"); // PortfolioRealTimeInfo should be the very first thing to be loaded, // as updateRealTimeStockMonitorAccordingToPortfolioTreeTableModels depends on this.portfolioRealTimeInfo. File portfolioRealTimeInfoFile = new File( org.yccheok.jstock.portfolio.Utils.getPortfolioRealTimeInfoFilepath()); PortfolioRealTimeInfo _portfolioRealTimeInfo = new PortfolioRealTimeInfo(); boolean status = _portfolioRealTimeInfo.load(portfolioRealTimeInfoFile); if (false == status) { Pair<HashMap<Code, Double>, Long> csvStockPrices = org.yccheok.jstock.portfolio.Utils .getCSVStockPrices(); _portfolioRealTimeInfo.stockPrices.putAll(csvStockPrices.first); _portfolioRealTimeInfo.stockPricesTimestamp = csvStockPrices.second; _portfolioRealTimeInfo.stockPricesDirty = !_portfolioRealTimeInfo.stockPrices.isEmpty(); } this.portfolioRealTimeInfo = _portfolioRealTimeInfo; if (openAsCSVFile(buyPortfolioFile) == false) { // If CSV file is not there, consider this as empty record. This is // because in createEmptyPortfolio, we only create portfolio-real-time-info.json, // for space and speed optimization purpose. if (buyPortfolioFile.exists()) { // Either CSV format is corrupted. return false; } } if (openAsCSVFile(sellPortfolioFile) == false) { // If CSV file is not there, consider this as empty record. This is // because in createEmptyPortfolio, we only create portfolio-real-time-info.json, // for space and speed optimization purpose. if (sellPortfolioFile.exists()) { // Either CSV format is corrupted. return false; } } if (openAsCSVFile(dividendSummaryFile) == false) { // If CSV file is not there, consider this as empty record. This is // because in createEmptyPortfolio, we only create portfolio-real-time-info.json, // for space and speed optimization purpose. if (dividendSummaryFile.exists()) { // Either CSV format is corrupted. return false; } } if (openAsCSVFile(depositSummaryFile) == false) { // If CSV file is not there, consider this as empty record. This is // because in createEmptyPortfolio, we only create portfolio-real-time-info.json, // for space and speed optimization purpose. if (depositSummaryFile.exists()) { // Either CSV format is corrupted. return false; } } refershGUIAfterInitPortfolio( (BuyPortfolioTreeTableModelEx) PortfolioManagementJPanel.this.buyTreeTable.getTreeTableModel(), (SellPortfolioTreeTableModelEx) PortfolioManagementJPanel.this.sellTreeTable.getTreeTableModel(), this.dividendSummary, this.depositSummary); return true; } // Different among refreshGUIAfterOptionsJDialog and refreshGUIAfterFeeCalculationEnabledOptionsChanged // is that, refreshGUIAfterOptionsJDialog doesn't touch on column visibility. public void refreshGUIAfterOptionsJDialog() { if (SwingUtilities.isEventDispatchThread()) { _refreshGUIAfterOptionsJDialog(); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { _refreshGUIAfterOptionsJDialog(); } }); } } private void _refreshGUIAfterOptionsJDialog() { this.buyTreeTable.repaint(); this.sellTreeTable.repaint(); this.updateWealthHeader(); } public void refreshGUIAfterFeeCalculationEnabledOptionsChanged() { if (SwingUtilities.isEventDispatchThread()) { _refreshGUIAfterFeeCalculationEnabledOptionsChanged(); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { _refreshGUIAfterFeeCalculationEnabledOptionsChanged(); } }); } } public void _refreshGUIAfterFeeCalculationEnabledOptionsChanged() { this.buyTreeTable.repaint(); this.sellTreeTable.repaint(); this.updateWealthHeader(); // Add/ remove columns based on user option. final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); JTable[] tables = { this.sellTreeTable, this.buyTreeTable }; String[] names = { GUIBundle.getString("PortfolioManagementJPanel_Broker"), GUIBundle.getString("PortfolioManagementJPanel_ClearingFee"), GUIBundle.getString("PortfolioManagementJPanel_StampDuty") }; for (JTable table : tables) { for (String name : names) { if (jStockOptions.isFeeCalculationEnabled()) { final int columnCount = table.getColumnCount(); JTableUtilities.insertTableColumnFromModel(table, name, columnCount); } else { JTableUtilities.removeTableColumn(table, name); } } } } private void _refershGUIAfterInitPortfolio(final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel, final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel, final DividendSummary _dividendSummary, final DepositSummary _depositSummary) { // Without "if" checking, tree expand won't work. Weird! if (PortfolioManagementJPanel.this.buyTreeTable.getTreeTableModel() != buyPortfolioTreeTableModel) { PortfolioManagementJPanel.this.buyTreeTable.setTreeTableModel(buyPortfolioTreeTableModel); } // Without "if" checking, tree expand won't work. Weird! if (PortfolioManagementJPanel.this.sellTreeTable.getTreeTableModel() != sellPortfolioTreeTableModel) { PortfolioManagementJPanel.this.sellTreeTable.setTreeTableModel(sellPortfolioTreeTableModel); } PortfolioManagementJPanel.this.depositSummary = _depositSummary; PortfolioManagementJPanel.this.dividendSummary = _dividendSummary; PortfolioManagementJPanel.this.updateRealTimeStockMonitorAccordingToPortfolioTreeTableModels(); PortfolioManagementJPanel.this.updateExchangeRateMonitorAccordingToPortfolioTreeTableModels(); PortfolioManagementJPanel.this.updateWealthHeader(); // Give user preferred GUI look. We do it here, because the entire table model is being changed. PortfolioManagementJPanel.this.initGUIOptions(); PortfolioManagementJPanel.this.updateTitledBorder(); // Every country is having different currency symbol. Remember to // refresh the currency symbol after we change the country. PortfolioManagementJPanel.this.refreshCurrencySymbol(); expandTreeTable(this.buyTreeTable); expandTreeTable(this.sellTreeTable); } private void expandTreeTable(JXTreeTable treeTable) { // Due to bug in JXTreeTable, expandRow sometimes just won't work. Here // is the hacking which makes it works. if (treeTable.isExpanded(0)) { treeTable.collapseRow(0); } // Expand the trees. treeTable.expandRow(0); } private void refershGUIAfterInitPortfolio(final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel, final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel, final DividendSummary _dividendSummary, final DepositSummary _depositSummary) { if (SwingUtilities.isEventDispatchThread()) { _refershGUIAfterInitPortfolio(buyPortfolioTreeTableModel, sellPortfolioTreeTableModel, _dividendSummary, _depositSummary); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { _refershGUIAfterInitPortfolio(buyPortfolioTreeTableModel, sellPortfolioTreeTableModel, _dividendSummary, _depositSummary); } }); } } public final void initPortfolio() { this.initCSVPortfolio(); } public void updateTitledBorder() { final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); if (SwingUtilities.isEventDispatchThread()) { final TitledBorder titledBorder = (TitledBorder) PortfolioManagementJPanel.this.jPanel1.getBorder(); titledBorder.setTitle(jStockOptions.getPortfolioName()); // So that title will refresh immediately. PortfolioManagementJPanel.this.jPanel1.repaint(); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { final TitledBorder titledBorder = (TitledBorder) PortfolioManagementJPanel.this.jPanel1 .getBorder(); titledBorder.setTitle(jStockOptions.getPortfolioName()); // So that title will refresh immediately. PortfolioManagementJPanel.this.jPanel1.repaint(); } }); } } private boolean saveCSVPortfolio(String directory, PortfolioRealTimeInfo portfolioRealTimeInfo) { if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist(directory) == false) { return false; } assert (directory.endsWith(File.separator)); final File buyPortfolioFile = new File(directory + "buyportfolio.csv"); final File sellPortfolioFile = new File(directory + "sellportfolio.csv"); final File dividendSummaryFile = new File(directory + "dividendsummary.csv"); final File depositSummaryFile = new File(directory + "depositsummary.csv"); final FileEx buyPortfolioFileEx = new FileEx(buyPortfolioFile, org.yccheok.jstock.file.Statement.Type.PortfolioManagementBuy); final FileEx sellPortfolioFileEx = new FileEx(sellPortfolioFile, org.yccheok.jstock.file.Statement.Type.PortfolioManagementSell); final FileEx dividendSummaryFileEx = new FileEx(dividendSummaryFile, org.yccheok.jstock.file.Statement.Type.PortfolioManagementDividend); final FileEx depositSummaryFileEx = new FileEx(depositSummaryFile, org.yccheok.jstock.file.Statement.Type.PortfolioManagementDeposit); if (false == saveAsCSVFile(buyPortfolioFileEx, true)) { final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) this.buyTreeTable .getTreeTableModel(); // org.yccheok.jstock.file.Statements is not good in handling empty // case. Let us handle it seperately. int count = buyPortfolioTreeTableModel.getRoot().getChildCount(); if (count > 0) { buyPortfolioFileEx.file.delete(); // Is not empty, but we fail to save it for unknown reason. return false; } } if (false == saveAsCSVFile(sellPortfolioFileEx, true)) { final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel = (SellPortfolioTreeTableModelEx) this.sellTreeTable .getTreeTableModel(); // org.yccheok.jstock.file.Statements is not good in handling empty // case. Let us handle it seperately. int count = sellPortfolioTreeTableModel.getRoot().getChildCount(); if (count > 0) { sellPortfolioFileEx.file.delete(); buyPortfolioFileEx.file.delete(); // Is not empty, but we fail to save it for unknown reason. return false; } } if (false == saveAsCSVFile(dividendSummaryFileEx, true)) { // org.yccheok.jstock.file.Statements is not good in handling empty // case. Let us handle it seperately. int count = this.dividendSummary.size(); if (count > 0) { dividendSummaryFileEx.file.delete(); depositSummaryFileEx.file.delete(); sellPortfolioFileEx.file.delete(); buyPortfolioFileEx.file.delete(); // Is not empty, but we fail to save it for unknown reason. return false; } } if (false == saveAsCSVFile(depositSummaryFileEx, true)) { // org.yccheok.jstock.file.Statements is not good in handling empty // case. Let us handle it seperately. int count = this.depositSummary.size(); if (count > 0) { depositSummaryFileEx.file.delete(); sellPortfolioFileEx.file.delete(); buyPortfolioFileEx.file.delete(); // Is not empty, but we fail to save it for unknown reason. return false; } } boolean status = savePortfolioRealTimeInfo(directory, portfolioRealTimeInfo); if (status) { // Legacy file. final File stockPricesFile = new File( org.yccheok.jstock.portfolio.Utils.getStockPricesFilepathViaDirectory(directory)); stockPricesFile.delete(); } return status; } private boolean saveCSVPortfolio() { return saveCSVPortfolio(org.yccheok.jstock.portfolio.Utils.getPortfolioDirectory(), this.portfolioRealTimeInfo); } private boolean savePortfolioRealTimeInfo(String directory, PortfolioRealTimeInfo portfolioRealTimeInfo) { PortfolioRealTimeInfo _portfolioRealTimeInfo = new PortfolioRealTimeInfo(portfolioRealTimeInfo); Map<Code, Double> goodStockPrices = new HashMap<>(); Map<Code, Currency> goodCurrencies = new HashMap<>(); Map<CurrencyPair, Double> goodExchangeRates = new HashMap<>(); //////////////////////////////////////////////////////////////////////// // goodStockPrices //////////////////////////////////////////////////////////////////////// for (Code code : getBuyCodes()) { final Double price = _portfolioRealTimeInfo.stockPrices.get(code); if (price == null) { // Having 0 is important too, so that we can generate correct // portfolioinfos.csv goodStockPrices.put(code, 0.0); } else { goodStockPrices.put(code, price); } } //////////////////////////////////////////////////////////////////////// // goodCurrencies. Don't use getBuyCodes, as we need to perform currency // conversion on sell stocks too. //////////////////////////////////////////////////////////////////////// for (Code code : getCodes()) { final Currency currency = _portfolioRealTimeInfo.currencies.get(code); if (currency != null) { goodCurrencies.put(code, currency); } } //////////////////////////////////////////////////////////////////////// // goodExchangeRates //////////////////////////////////////////////////////////////////////// for (CurrencyPair currencyPair : getCurrencyPairs()) { final Double rate = _portfolioRealTimeInfo.exchangeRates.get(currencyPair); if (rate != null) { goodExchangeRates.put(currencyPair, rate); } } _portfolioRealTimeInfo.stockPrices.clear(); _portfolioRealTimeInfo.currencies.clear(); _portfolioRealTimeInfo.exchangeRates.clear(); _portfolioRealTimeInfo.stockPrices.putAll(goodStockPrices); _portfolioRealTimeInfo.currencies.putAll(goodCurrencies); _portfolioRealTimeInfo.exchangeRates.putAll(goodExchangeRates); final File stockPricesFile = new File(directory + "portfolio-real-time-info.json"); return _portfolioRealTimeInfo.save(stockPricesFile); } public boolean savePortfolio() { return saveCSVPortfolio(); } private void refreshStatusBarExchangeRateVisibility() { final JStock mainFrame = JStock.instance(); final JStockOptions jStockOptions = mainFrame.getJStockOptions(); final Country country = jStockOptions.getCountry(); final boolean currencyExchangeEnable = jStockOptions.isCurrencyExchangeEnable(country); if (!currencyExchangeEnable) { mainFrame.setStatusBarExchangeRateVisible(false); mainFrame.setStatusBarExchangeRateToolTipText(null); return; } Set<CurrencyPair> currencyPairs = this.getCurrencyPairs(); // Special handling for GBX. currencyPairs.remove(CurrencyPair.create(Currency.GBX, Currency.GBP)); // Special handling for ZAC. currencyPairs.remove(CurrencyPair.create(Currency.ZAC, Currency.ZAR)); if (currencyPairs.isEmpty()) { mainFrame.setStatusBarExchangeRateVisible(false); mainFrame.setStatusBarExchangeRateToolTipText(null); return; } final int size = currencyPairs.size(); if (size > 1) { mainFrame.setStatusBarExchangeRateVisible(false); mainFrame.setStatusBarExchangeRateToolTipText(null); return; } // We will display the currency exchange rate, only if there is 1 // currency pair. if (size == 1) { // Update the tool tip text. CurrencyPair currencyPair = currencyPairs.iterator().next(); Currency fromCurrency = currencyPair.from(); Currency toCurrency = currencyPair.to(); final String text = MessageFormat.format(GUIBundle.getString("MyJXStatusBar_CurrencyExchangeRateFor"), fromCurrency.toString(), toCurrency.toString()); mainFrame.setStatusBarExchangeRateVisible(true); mainFrame.setStatusBarExchangeRateToolTipText(text); Double rate = this.portfolioRealTimeInfo.exchangeRates.get(currencyPair); if (rate != null) { // Special handling for GBX/ZAC. User would prefer to see the // currency exchange in GBP/ZAR. if (currencyPair.from().isGBX() || currencyPair.from().isZAC()) { rate = rate * 100.0; } if (rate == 1.0) { // User are not interested in such exchange rate. mainFrame.setStatusBarExchangeRate(null); } else { mainFrame.setStatusBarExchangeRate(rate); } } else { mainFrame.setStatusBarExchangeRate(null); } return; } } /** * Initializes exchange rate monitor. */ public void initExchangeRateMonitor() { final JStock jstock = JStock.instance(); final JStockOptions jStockOptions = jstock.getJStockOptions(); final Country country = jStockOptions.getCountry(); final boolean currencyExchangeEnable = jStockOptions.isCurrencyExchangeEnable(country); refreshStatusBarExchangeRateVisibility(); final ExchangeRateMonitor oldExchangeRateMonitor = this.exchangeRateMonitor; if (oldExchangeRateMonitor != null) { Utils.getZoombiePool().execute(new Runnable() { @Override public void run() { log.info("Prepare to shut down " + oldExchangeRateMonitor + "..."); oldExchangeRateMonitor.dettachAll(); oldExchangeRateMonitor.stop(); log.info("Shut down " + oldExchangeRateMonitor + " peacefully."); } }); } if (!currencyExchangeEnable) { this.exchangeRateMonitor = null; if (oldExchangeRateMonitor != null) { this.updateWealthHeader(); } return; } assert (currencyExchangeEnable); this.exchangeRateMonitor = new ExchangeRateMonitor(Constants.EXCHANGE_RATE_MONITOR_MAX_THREAD, Constants.EXCHANGE_RATE_MONITOR_MAX_STOCK_SIZE_PER_SCAN, jStockOptions.getScanningSpeed()); this.exchangeRateMonitor.attach(exchangeRateMonitorObserver); updateExchangeRateMonitorAccordingToPortfolioTreeTableModels(); } public void initRealTimeStockMonitor() { final RealTimeStockMonitor oldRealTimeStockMonitor = realTimeStockMonitor; if (oldRealTimeStockMonitor != null) { Utils.getZoombiePool().execute(new Runnable() { @Override public void run() { log.info("Prepare to shut down " + oldRealTimeStockMonitor + "..."); oldRealTimeStockMonitor.clearStockCodes(); oldRealTimeStockMonitor.dettachAll(); oldRealTimeStockMonitor.stop(); log.info("Shut down " + oldRealTimeStockMonitor + " peacefully."); } }); } realTimeStockMonitor = new RealTimeStockMonitor(Constants.REAL_TIME_STOCK_MONITOR_MAX_THREAD, Constants.REAL_TIME_STOCK_MONITOR_MAX_STOCK_SIZE_PER_SCAN, JStock.instance().getJStockOptions().getScanningSpeed()); realTimeStockMonitor.attach(this.realTimeStockMonitorObserver); updateRealTimeStockMonitorAccordingToPortfolioTreeTableModels(); } private org.yccheok.jstock.engine.Observer<ExchangeRateMonitor, List<ExchangeRate>> getExchangeRateMonitorObserver() { return new org.yccheok.jstock.engine.Observer<ExchangeRateMonitor, List<ExchangeRate>>() { @Override public void update(ExchangeRateMonitor subject, java.util.List<ExchangeRate> exchangeRates) { final ExchangeRateMonitor _exchangeRateMonitor = exchangeRateMonitor; if (_exchangeRateMonitor != null) { final Map<CurrencyPair, Double> exchangeRatesMap = portfolioRealTimeInfo.exchangeRates; Set<CurrencyPair> currencyPairs = getCurrencyPairs(); for (ExchangeRate exchangeRate : exchangeRates) { final CurrencyPair currencyPair = exchangeRate.currencyPair(); if (false == currencyPairs.contains(currencyPair)) { // Thread safety purpose. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (false == getCurrencyPairs().contains(currencyPair)) { _exchangeRateMonitor.removeCurrencyPair(currencyPair); exchangeRatesMap.remove(currencyPair); } } }); } else { double rate = exchangeRate.rate(); exchangeRatesMap.put(currencyPair, rate); } portfolioRealTimeInfo.exchangeRatesDirty = true; } portfolioRealTimeInfo.exchangeRatesTimestamp = System.currentTimeMillis(); } refreshStatusBarExchangeRateVisibility(); updateWealthHeader(); final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); buyPortfolioTreeTableModel.refreshRoot(); sellPortfolioTreeTableModel.refreshRoot(); } }; } // 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) { PortfolioManagementJPanel.this.update(monitor, stocks); } }; } private double getNonZeroPriceIfPossible(Stock stock) { // First. final double lastPrice = stock.getLastPrice(); if (lastPrice > 0.0) { return lastPrice; } // Second. final double openPrice = stock.getOpenPrice(); if (openPrice > 0.0) { return openPrice; } // Third. final double prevPrice = stock.getPrevPrice(); return Math.max(0.0, prevPrice); } private void update(RealTimeStockMonitor monitor, final java.util.List<Stock> stocks) { final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) buyTreeTable .getTreeTableModel(); final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel = (SellPortfolioTreeTableModelEx) sellTreeTable .getTreeTableModel(); final Map<Code, Double> stockPrices = this.portfolioRealTimeInfo.stockPrices; final Map<Code, Currency> currencies = this.portfolioRealTimeInfo.currencies; final Set<Code> buyCodes = this.getBuyCodes(); final Set<Code> sellCodes = this.getSellCodes(); for (Stock stock : stocks) { final Code code = stock.code; final Currency currency = stock.getCurrency(); boolean needBuyRefresh = false; boolean needSellRefresh = false; if (buyCodes.contains(code)) { final Double price = getNonZeroPriceIfPossible(stock); final Double oldPrice = stockPrices.put(code, price); if (false == price.equals(oldPrice)) { this.portfolioRealTimeInfo.stockPricesDirty = true; needBuyRefresh = true; } // ConcurrentHashMap doesn't support null value. // http://stackoverflow.com/questions/698638/why-does-concurrenthashmap-prevent-null-keys-and-values if (currency != null) { final Currency oldCurrency = currencies.put(code, currency); if (false == currency.equals(oldCurrency)) { this.portfolioRealTimeInfo.currenciesDirty = true; needBuyRefresh = true; // Should sell portfolio refresh too, since we have new currency info? if (sellCodes.contains(code)) { needSellRefresh = true; } } } } else if (sellCodes.contains(code)) { if (currency != null) { final Currency oldCurrency = currencies.put(code, currency); if (false == currency.equals(oldCurrency)) { this.portfolioRealTimeInfo.currenciesDirty = true; needSellRefresh = true; } // Thread safety purpose. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Set<Code> buyCodes = PortfolioManagementJPanel.this.getBuyCodes(); if (false == buyCodes.contains(code)) { // We can remove this code safely. PortfolioManagementJPanel.this.realTimeStockMonitor.removeStockCode(code); } } }); } else { // Should I do something here? Or, should I keep retrying // without removing code from } } else { // Thread safety purpose. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Set<Code> codes = PortfolioManagementJPanel.this.getCodes(); if (false == codes.contains(code)) { // We can remove this code safely. PortfolioManagementJPanel.this.realTimeStockMonitor.removeStockCode(code); } } }); continue; } if (needBuyRefresh) { // Because we have new price & new currency. buyPortfolioTreeTableModel.refresh(code); } if (needSellRefresh) { // Because we have new currency. sellPortfolioTreeTableModel.refresh(code); } // Update currency monitor as well. ExchangeRateMonitor _exchangeRateMonitor = this.exchangeRateMonitor; if (currency != null && _exchangeRateMonitor != null) { final Currency localCurrency = org.yccheok.jstock.portfolio.Utils.getLocalCurrency(); if (localCurrency != null) { if (false == currency.equals(localCurrency)) { CurrencyPair currencyPair = CurrencyPair.create(currency, localCurrency); if (_exchangeRateMonitor.addCurrencyPair(currencyPair)) { _exchangeRateMonitor.startNewThreadsIfNecessary(); _exchangeRateMonitor.refresh(); } } } } } // for updateWealthHeader(); // Update status bar with current time string. this.portfolioRealTimeInfo.stockPricesTimestamp = System.currentTimeMillis(); JStock.instance().updateStatusBarWithLastUpdateDateMessageIfPossible(); } public long getTimestamp() { return this.portfolioRealTimeInfo.stockPricesTimestamp; } private void initGUIOptions() { File f = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "portfoliomanagementjpanel.xml"); final GUIOptions guiOptions = Utils.fromXML(GUIOptions.class, f); if (guiOptions == null) { return; } if (guiOptions.getJTableOptionsSize() <= 1) { return; } final org.jdesktop.swingx.JXTreeTable[] treeTables = { buyTreeTable, sellTreeTable }; /* Set Table Settings */ for (int tableIndex = 0; tableIndex < treeTables.length; tableIndex++) { final JXTreeTable treeTable = treeTables[tableIndex]; final javax.swing.table.JTableHeader jTableHeader = treeTable.getTableHeader(); final JTable jTable = jTableHeader.getTable(); JTableUtilities.setJTableOptions(jTable, guiOptions.getJTableOptions(tableIndex)); } // Do we have the divider location option? if (guiOptions.getDividerLocationSize() > 0) { // Yes. Remember the divider location. // It will be used in updateDividerLocation later. this.dividerLocation = guiOptions.getDividerLocation(0); } } // Also, be aware: Calling setDividerLocation(double) before the JSplitPane // is visible will not work correctly. // Workaround for bug : http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6528446 public void updateDividerLocation() { if (this.dividerLocation > 0) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { // Have the code in AWT event dispatching thread, in order // to ensure MainFrame is already shown on the screen. jSplitPane1.setDividerLocation(dividerLocation); } }); } } public boolean saveGUIOptions() { if (Utils.createCompleteDirectoryHierarchyIfDoesNotExist( org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config") == false) { return false; } final GUIOptions guiOptions = new GUIOptions(); final org.jdesktop.swingx.JXTreeTable[] treeTables = { buyTreeTable, sellTreeTable }; for (org.jdesktop.swingx.JXTreeTable treeTable : treeTables) { final javax.swing.table.JTableHeader jTableHeader = treeTable.getTableHeader(); final JTable jTable = jTableHeader.getTable(); final GUIOptions.JTableOptions jTableOptions = new GUIOptions.JTableOptions(); final int count = jTable.getColumnCount(); for (int i = 0; i < count; i++) { final String name = jTable.getColumnName(i); final TableColumn column = jTable.getColumnModel().getColumn(i); jTableOptions.addColumnOption( GUIOptions.JTableOptions.ColumnOption.newInstance(name, column.getWidth())); } guiOptions.addJTableOptions(jTableOptions); } guiOptions.addDividerLocation(jSplitPane1.getDividerLocation()); File f = new File(org.yccheok.jstock.gui.Utils.getUserDataDirectory() + "config" + File.separator + "portfoliomanagementjpanel.xml"); return org.yccheok.jstock.gui.Utils.toXML(guiOptions, f); } public boolean saveAsExcelFile(File file, boolean languageIndependent) { org.yccheok.jstock.file.Statements.StatementsEx statementsEx0, statementsEx1, statementsEx2, statementsEx3; Statements statements0 = org.yccheok.jstock.file.Statements.newInstanceFromBuyPortfolioTreeTableModel( (BuyPortfolioTreeTableModelEx) this.buyTreeTable.getTreeTableModel(), this.portfolioRealTimeInfo, languageIndependent); Statements statements1 = org.yccheok.jstock.file.Statements.newInstanceFromSellPortfolioTreeTableModel( (SellPortfolioTreeTableModelEx) this.sellTreeTable.getTreeTableModel(), this.portfolioRealTimeInfo, languageIndependent); Statements statements2 = org.yccheok.jstock.file.Statements.newInstanceFromTableModel( new DividendSummaryTableModel(this.dividendSummary), languageIndependent); Statements statements3 = org.yccheok.jstock.file.Statements .newInstanceFromTableModel(new DepositSummaryTableModel(this.depositSummary), languageIndependent); statementsEx0 = new org.yccheok.jstock.file.Statements.StatementsEx(statements0, GUIBundle.getString("PortfolioManagementJPanel_BuyPortfolio")); statementsEx1 = new org.yccheok.jstock.file.Statements.StatementsEx(statements1, GUIBundle.getString("PortfolioManagementJPanel_SellPortfolio")); statementsEx2 = new org.yccheok.jstock.file.Statements.StatementsEx(statements2, GUIBundle.getString("PortfolioManagementJPanel_DividendPortfolio")); statementsEx3 = new org.yccheok.jstock.file.Statements.StatementsEx(statements3, GUIBundle.getString("PortfolioManagementJPanel_CashDepositPortfolio")); List<org.yccheok.jstock.file.Statements.StatementsEx> statementsExs = Arrays.asList(statementsEx0, statementsEx1, statementsEx2, statementsEx3); return Statements.saveAsExcelFile(file, statementsExs); } public boolean saveAsCSVFile(Utils.FileEx fileEx, boolean languageIndependent) { org.yccheok.jstock.file.Statements statements = null; if (fileEx.type == org.yccheok.jstock.file.Statement.Type.PortfolioManagementBuy) { // For buy portfolio, need not save metadata information, as we have // seperate "stockprices.csv" to handle it. However, I am not really // sure that whether seperating them is a good idea. statements = org.yccheok.jstock.file.Statements.newInstanceFromBuyPortfolioTreeTableModel( (BuyPortfolioTreeTableModelEx) this.buyTreeTable.getTreeTableModel(), this.portfolioRealTimeInfo, languageIndependent); } else if (fileEx.type == org.yccheok.jstock.file.Statement.Type.PortfolioManagementSell) { statements = org.yccheok.jstock.file.Statements.newInstanceFromSellPortfolioTreeTableModel( (SellPortfolioTreeTableModelEx) this.sellTreeTable.getTreeTableModel(), this.portfolioRealTimeInfo, languageIndependent); } else if (fileEx.type == org.yccheok.jstock.file.Statement.Type.PortfolioManagementDividend) { statements = org.yccheok.jstock.file.Statements.newInstanceFromTableModel( new DividendSummaryTableModel(this.dividendSummary), languageIndependent); } else if (fileEx.type == org.yccheok.jstock.file.Statement.Type.PortfolioManagementDeposit) { statements = org.yccheok.jstock.file.Statements.newInstanceFromTableModel( new DepositSummaryTableModel(this.depositSummary), languageIndependent); } // Use metadata to store TransactionSummary's comment. return statements.saveAsCSVFile(fileEx.file); } private void updateWealthHeader() { final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); final boolean isFeeCalculationEnabled = jStockOptions.isFeeCalculationEnabled(); final BuyPortfolioTreeTableModelEx buyPortfolioTreeTableModel = (BuyPortfolioTreeTableModelEx) this.buyTreeTable .getTreeTableModel(); final SellPortfolioTreeTableModelEx sellPortfolioTreeTableModel = (SellPortfolioTreeTableModelEx) this.sellTreeTable .getTreeTableModel(); final Currency localCurrency = org.yccheok.jstock.portfolio.Utils.getLocalCurrency(); final double share; final double cash; final double paperProfit; final double realizedProfit; share = buyPortfolioTreeTableModel.getCurrentValue(localCurrency); final Country country = jStockOptions.getCountry(); if (isFeeCalculationEnabled) { final double exchangeRate = org.yccheok.jstock.portfolio.Utils.getExchangeRate(portfolioRealTimeInfo, localCurrency, country.stockCurrency); double _cash = sellPortfolioTreeTableModel.getNetSellingValue(localCurrency) - ((Portfolio) sellPortfolioTreeTableModel.getRoot()).getNetReferenceTotal(localCurrency) - buyPortfolioTreeTableModel.getNetPurchaseValue(localCurrency); double deposit = this.getDepositSummary().getTotal() * exchangeRate; if (country.stockCurrency.isGBX() || country.stockCurrency.isZAC()) { // Use will input cash in GBP/ZAR instead of GBX/ZAC. deposit = deposit * 100.0; } double dividend = this.getDividendSummary().getTotal(this.portfolioRealTimeInfo, localCurrency); _cash += deposit; _cash += dividend; cash = _cash; paperProfit = buyPortfolioTreeTableModel.getNetGainLossValue(localCurrency); realizedProfit = sellPortfolioTreeTableModel.getNetGainLossValue(localCurrency); } else { final double exchangeRate = org.yccheok.jstock.portfolio.Utils.getExchangeRate(portfolioRealTimeInfo, localCurrency, country.stockCurrency); double _cash = sellPortfolioTreeTableModel.getSellingValue(localCurrency) - ((Portfolio) sellPortfolioTreeTableModel.getRoot()).getReferenceTotal(localCurrency) - buyPortfolioTreeTableModel.getPurchaseValue(localCurrency); double deposit = this.getDepositSummary().getTotal() * exchangeRate; if (country.stockCurrency.isGBX() || country.stockCurrency.isZAC()) { // Use will input cash in GBP/ZAR instead of GBX/ZAC. deposit = deposit * 100.0; } double dividend = this.getDividendSummary().getTotal(this.portfolioRealTimeInfo, localCurrency); _cash += deposit; _cash += dividend; cash = _cash; paperProfit = buyPortfolioTreeTableModel.getGainLossValue(localCurrency); realizedProfit = sellPortfolioTreeTableModel.getGainLossValue(localCurrency); } final double paperProfitPercentage; final double realizedProfitPercentage; if (isFeeCalculationEnabled) { paperProfitPercentage = buyPortfolioTreeTableModel.getNetGainLossPercentage(localCurrency); realizedProfitPercentage = sellPortfolioTreeTableModel.getNetGainLossPercentage(localCurrency); } else { paperProfitPercentage = buyPortfolioTreeTableModel.getGainLossPercentage(localCurrency); realizedProfitPercentage = sellPortfolioTreeTableModel.getGainLossPercentage(localCurrency); } final DecimalPlace decimalPlace = jStockOptions.getDecimalPlace(); final String _share = org.yccheok.jstock.portfolio.Utils.toCurrency(decimalPlace, share); final String _cash = org.yccheok.jstock.portfolio.Utils.toCurrency(decimalPlace, cash); final String _paperProfit = org.yccheok.jstock.portfolio.Utils.toCurrency(decimalPlace, paperProfit); final String _paperProfitPercentage = org.yccheok.jstock.portfolio.Utils.toCurrency(DecimalPlace.Two, paperProfitPercentage); final String _realizedProfit = org.yccheok.jstock.portfolio.Utils.toCurrency(decimalPlace, realizedProfit); final String _realizedProfitPercentage = org.yccheok.jstock.portfolio.Utils.toCurrency(DecimalPlace.Two, realizedProfitPercentage); javax.swing.SwingUtilities.invokeLater(new Runnable() { @Override public void run() { jLabel2.setText(_share); jLabel4.setText(_cash); jLabel6.setText(_paperProfit + " (" + _paperProfitPercentage + "%)"); jLabel8.setText(_realizedProfit + " (" + _realizedProfitPercentage + "%)"); jLabel2.setForeground(Utils.getColor(share, 0.0)); jLabel4.setForeground(Utils.getColor(cash, 0.0)); jLabel6.setForeground(Utils.getColor(paperProfit, 0.0)); jLabel8.setForeground(Utils.getColor(realizedProfit, 0.0)); } }); } public void resumeRealTimeStockMonitor() { if (realTimeStockMonitor == null) { return; } realTimeStockMonitor.resume(); } public void suspendRealTimeStockMonitor() { if (realTimeStockMonitor == null) { return; } realTimeStockMonitor.suspend(); } /** * Refresh all the labels with latest currency symbol. */ public void refreshCurrencySymbol() { jLabel1.setText(getShareLabel()); jLabel3.setText(getCashLabel()); jLabel5.setText(getPaperProfitLabel()); jLabel7.setText(getRealizedProfitLabel()); } private String getShareLabel() { final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); return MessageFormat.format(GUIBundle.getString("PortfolioManagementJPanel_ShareLabel"), jStockOptions.getCurrencySymbol(jStockOptions.getCountry())); } private String getCashLabel() { final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); return MessageFormat.format(GUIBundle.getString("PortfolioManagementJPanel_CashLabel_template"), jStockOptions.getCurrencySymbol(jStockOptions.getCountry())); } private String getPaperProfitLabel() { final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); return MessageFormat.format(GUIBundle.getString("PortfolioManagementJPanel_PaperProfitLabel_template"), jStockOptions.getCurrencySymbol(jStockOptions.getCountry())); } private String getRealizedProfitLabel() { final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); return MessageFormat.format(GUIBundle.getString("PortfolioManagementJPanel_RealizedProfitLabel_template"), jStockOptions.getCurrencySymbol(jStockOptions.getCountry())); } public void refreshExchangeRateMonitor() { ExchangeRateMonitor _exchangeRateMonitor = this.exchangeRateMonitor; if (_exchangeRateMonitor != null) { _exchangeRateMonitor.refresh(); } } public void refreshRealTimeStockMonitor() { RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; if (_realTimeStockMonitor != null) { _realTimeStockMonitor.refresh(); } } public void rebuildRealTimeStockMonitor() { RealTimeStockMonitor _realTimeStockMonitor = this.realTimeStockMonitor; if (_realTimeStockMonitor != null) { _realTimeStockMonitor.rebuild(); } } public PortfolioRealTimeInfo getPortfolioRealTimeInfo() { return this.portfolioRealTimeInfo; } // We will display currency info if currency exchange feature is enabled, // and the stock currency is different from country stock currency. public boolean shouldDisplayCurrencyForValue(Code code) { final JStockOptions jStockOptions = JStock.instance().getJStockOptions(); final Country country = jStockOptions.getCountry(); final boolean isCurrencyExchangeEnable = jStockOptions.isCurrencyExchangeEnable(country); if (false == isCurrencyExchangeEnable) { return false; } final Currency stockCurrency = org.yccheok.jstock.portfolio.Utils.getStockCurrency(portfolioRealTimeInfo, code); final Currency countryStockCurrency = country.stockCurrency; return (false == stockCurrency.equals(countryStockCurrency)); } private static final Log log = LogFactory.getLog(PortfolioManagementJPanel.class); private int dividerLocation = -1; // Data structure. private DepositSummary depositSummary = new DepositSummary(); private DividendSummary dividendSummary = new DividendSummary(); private RealTimeStockMonitor realTimeStockMonitor = null; private ExchangeRateMonitor exchangeRateMonitor = null; private final org.yccheok.jstock.engine.Observer<RealTimeStockMonitor, List<Stock>> realTimeStockMonitorObserver = this .getRealTimeStockMonitorObserver(); private final org.yccheok.jstock.engine.Observer<ExchangeRateMonitor, List<ExchangeRate>> exchangeRateMonitorObserver = this .getExchangeRateMonitorObserver(); private PortfolioRealTimeInfo portfolioRealTimeInfo = new PortfolioRealTimeInfo(); // Variables declaration - do not modify//GEN-BEGIN:variables private org.yccheok.jstock.gui.treetable.SortableTreeTable buyTreeTable; private javax.swing.JButton jButton1; private javax.swing.JButton jButton3; private javax.swing.JButton jButton4; private javax.swing.JButton jButton5; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JLabel jLabel3; private javax.swing.JLabel jLabel4; private javax.swing.JLabel jLabel5; private javax.swing.JLabel jLabel6; private javax.swing.JLabel jLabel7; private javax.swing.JLabel jLabel8; private javax.swing.JPanel jPanel1; private javax.swing.JPanel jPanel2; private javax.swing.JPanel jPanel3; private javax.swing.JPanel jPanel4; private javax.swing.JPanel jPanel5; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JScrollPane jScrollPane2; private javax.swing.JSplitPane jSplitPane1; private org.yccheok.jstock.gui.treetable.SortableTreeTable sellTreeTable; // End of variables declaration//GEN-END:variables }