Java tutorial
/* * * JMoney - A Personal Finance Manager * Copyright (c) 2004 Nigel Westbury <westbury@users.sourceforge.net> * * * 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., 675 Mass Ave, Cambridge, MA 02139, USA. * */ package net.sf.jmoney.reconciliation.reconcilePage; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import net.sf.jmoney.model2.Currency; import net.sf.jmoney.model2.CurrencyAccount; import net.sf.jmoney.model2.Entry; import net.sf.jmoney.model2.EntryInfo; import net.sf.jmoney.model2.ExtendableObject; import net.sf.jmoney.model2.ScalarPropertyAccessor; import net.sf.jmoney.model2.SessionChangeAdapter; import net.sf.jmoney.reconciliation.BankStatement; import net.sf.jmoney.reconciliation.IReconciliationQueries; import net.sf.jmoney.reconciliation.ReconciliationEntryInfo; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Table; import org.eclipse.ui.forms.SectionPart; import org.eclipse.ui.forms.widgets.FormToolkit; import org.eclipse.ui.forms.widgets.Section; /** * Implementation of the 'Statements' section of the reconciliation * page. This section lists all the statements in the account that * have reconciled entries in them. * * @author Nigel Westbury */ public class StatementsSection extends SectionPart { protected Table statementTable; protected StatementContentProvider contentProvider; private TableViewerColumn balanceColumn; public StatementsSection(Composite parent, FormToolkit toolkit, CurrencyAccount account) { super(parent, toolkit, Section.DESCRIPTION | Section.TITLE_BAR); getSection().setText("Statements"); getSection().setDescription("Double click a statement to show that statement."); final Currency currencyForFormatting = account.getCurrency(); Composite composite = new Composite(getSection(), SWT.NONE); composite.setLayout(new GridLayout()); statementTable = new Table(composite, SWT.FULL_SELECTION | SWT.SINGLE | SWT.V_SCROLL); GridData gdTable = new GridData(SWT.FILL, SWT.FILL, true, true); gdTable.heightHint = 100; statementTable.setLayoutData(gdTable); statementTable.setHeaderVisible(true); statementTable.setLinesVisible(true); // Create and setup the TableViewer TableViewer tableViewer = new TableViewer(statementTable); tableViewer.setUseHashlookup(true); // Add the columns TableViewerColumn statementColumn = new TableViewerColumn(tableViewer, SWT.LEFT); statementColumn.getColumn().setWidth(65); statementColumn.getColumn().setText("Statement"); statementColumn.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { StatementDetails statementDetails = (StatementDetails) element; return statementDetails.statement.toLocalizedString(); } }); balanceColumn = new TableViewerColumn(tableViewer, SWT.RIGHT); balanceColumn.getColumn().setWidth(70); balanceColumn.getColumn().setText("Balance"); balanceColumn.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { StatementDetails statementDetails = (StatementDetails) element; return currencyForFormatting.format(statementDetails.getClosingBalance()); } }); contentProvider = new StatementContentProvider(tableViewer); tableViewer.setContentProvider(contentProvider); tableViewer.setComparator(new StatementViewerComparator()); tableViewer.setInput(account); /* * Scroll the statement list to the bottom so that the most recent * statements are shown (if there are any statements). */ StatementDetails lastStatementDetails = contentProvider.getLastStatement(); if (lastStatementDetails != null) { tableViewer.reveal(lastStatementDetails); } getSection().setClient(composite); toolkit.paintBordersFor(composite); refresh(); } class StatementContentProvider implements IStructuredContentProvider { /** * The table viewer to be notified whenever the content changes. */ TableViewer tableViewer; CurrencyAccount account; SortedMap<BankStatement, StatementDetails> statementDetailsMap; StatementContentProvider(TableViewer tableViewer) { this.tableViewer = tableViewer; } public void inputChanged(Viewer v, Object oldInput, Object newInput) { account = (CurrencyAccount) newInput; // Build a tree map of the statement totals // We use a tree map in preference to the more efficient // hash map because we can then fetch the results in order. statementDetailsMap = new TreeMap<BankStatement, StatementDetails>(); // When this item is disposed, the input may be set to null. // Return an empty list in this case. if (newInput == null) { return; } IReconciliationQueries queries = (IReconciliationQueries) account.getSession() .getAdapter(IReconciliationQueries.class); if (queries != null) { // TODO: change this method //return queries.getStatements(fPage.getAccount()); } else { // IReconciliationQueries has not been implemented in the datastore. // We must therefore provide our own implementation. // We use a tree map in preference to the more efficient // hash map because we can then fetch the results in order. SortedMap<BankStatement, Long> statementTotals = new TreeMap<BankStatement, Long>(); for (Entry entry : account.getEntries()) { BankStatement statement = entry .getPropertyValue(ReconciliationEntryInfo.getStatementAccessor()); if (statement != null) { Long statementTotal = statementTotals.get(statement); if (statementTotal == null) { statementTotal = new Long(0); } statementTotals.put(statement, new Long(statementTotal.longValue() + entry.getAmount())); } } long balance = account.getStartBalance(); for (Map.Entry<BankStatement, Long> mapEntry : statementTotals.entrySet()) { BankStatement statement = mapEntry.getKey(); long totalEntriesOnStatement = (mapEntry.getValue()).longValue(); statementDetailsMap.put(statement, new StatementDetails(statement, balance, totalEntriesOnStatement)); balance += totalEntriesOnStatement; } } // Listen for changes so we can keep the tree map up to date. account.getDataManager().addChangeListener(new SessionChangeAdapter() { @Override public void objectCreated(ExtendableObject newObject) { if (newObject instanceof Entry) { Entry newEntry = (Entry) newObject; if (account.equals(newEntry.getAccount())) { BankStatement statement = newEntry .getPropertyValue(ReconciliationEntryInfo.getStatementAccessor()); adjustStatement(statement, newEntry.getAmount()); } } } @Override public void objectDestroyed(ExtendableObject deletedObject) { if (deletedObject instanceof Entry) { Entry deletedEntry = (Entry) deletedObject; if (account.equals(deletedEntry.getAccount())) { BankStatement statement = deletedEntry .getPropertyValue(ReconciliationEntryInfo.getStatementAccessor()); adjustStatement(statement, -deletedEntry.getAmount()); } } } @Override public void objectChanged(ExtendableObject changedObject, ScalarPropertyAccessor changedProperty, Object oldValue, Object newValue) { if (changedObject instanceof Entry) { Entry entry = (Entry) changedObject; if (changedProperty == EntryInfo.getAccountAccessor()) { if (account.equals(oldValue)) { BankStatement statement = entry .getPropertyValue(ReconciliationEntryInfo.getStatementAccessor()); adjustStatement(statement, -entry.getAmount()); } if (account.equals(newValue)) { BankStatement statement = entry .getPropertyValue(ReconciliationEntryInfo.getStatementAccessor()); adjustStatement(statement, entry.getAmount()); } } else { if (account.equals(entry.getAccount())) { if (changedProperty == EntryInfo.getAmountAccessor()) { BankStatement statement = entry .getPropertyValue(ReconciliationEntryInfo.getStatementAccessor()); long oldAmount = ((Long) oldValue).longValue(); long newAmount = ((Long) newValue).longValue(); adjustStatement(statement, newAmount - oldAmount); } else if (changedProperty == ReconciliationEntryInfo.getStatementAccessor()) { adjustStatement((BankStatement) oldValue, -entry.getAmount()); adjustStatement((BankStatement) newValue, entry.getAmount()); } } } } } }, statementTable); } /** * @param statement the statement to be adjusted. This parameter * may be null in which case this method does nothing * @param amount the amount by which the total for the given statement * is to be adjusted */ void adjustStatement(BankStatement statement, long amount) { if (statement != null) { StatementDetails thisStatementDetails = statementDetailsMap.get(statement); if (thisStatementDetails == null) { long openingBalance; SortedMap<BankStatement, StatementDetails> priorStatements = statementDetailsMap .headMap(statement); if (priorStatements.isEmpty()) { openingBalance = account.getStartBalance(); } else { openingBalance = priorStatements.get(priorStatements.lastKey()).getClosingBalance(); } thisStatementDetails = new StatementDetails(statement, openingBalance, amount); statementDetailsMap.put(statement, thisStatementDetails); // Notify the viewer that we have a new item tableViewer.add(thisStatementDetails); } else { thisStatementDetails.adjustEntriesTotal(amount); // Notify the viewer that an item has changed tableViewer.update(thisStatementDetails, null); } // This total affects all later balances. // Iterate through all later statements updating the // statement details and then notify the viewer of the // update. Note that tailMap returns a collection that // includes the starting key, so we must be sure not to // process that. SortedMap<BankStatement, StatementDetails> laterStatements = statementDetailsMap.tailMap(statement); for (StatementDetails statementDetails : laterStatements.values()) { if (!statementDetails.statement.equals(statement)) { statementDetails.adjustOpeningBalance(amount); // Notify the viewer that an item has changed tableViewer.update(statementDetails, null); } } } } public void dispose() { } public Object[] getElements(Object parent) { // Return an array of the statements. These are already ordered. return statementDetailsMap.values().toArray(); } /** * @return */ public StatementDetails getLastStatement() { if (statementDetailsMap.isEmpty()) { return null; } else { return statementDetailsMap.get(statementDetailsMap.lastKey()); } } /** * @return the last statement before the given statement, or null if there is no * statement prior to the given statement */ public BankStatement getPriorStatement(BankStatement statement) { SortedMap<BankStatement, StatementDetails> headMap = statementDetailsMap.headMap(statement); if (headMap.isEmpty()) { return null; } else { return headMap.lastKey(); } } public long getStatementOpeningBalance(BankStatement statement) { long openingBalance; SortedMap<BankStatement, StatementDetails> priorStatements = statementDetailsMap.headMap(statement); if (priorStatements.isEmpty()) { openingBalance = account.getStartBalance(); } else { openingBalance = priorStatements.get(priorStatements.lastKey()).getClosingBalance(); } return openingBalance; } } /** * Even though the content provider supplies the statements in order, we * still need to set a sorter into the table. The reason is that new * statements will otherwise always be added at the end and it is possible, * though unlikely, that a user will go back and add earlier statements. */ class StatementViewerComparator extends ViewerComparator { @Override public int compare(Viewer viewer, Object element1, Object element2) { StatementDetails statementDetails1 = (StatementDetails) element1; StatementDetails statementDetails2 = (StatementDetails) element2; return statementDetails1.compareTo(statementDetails2); } } /** * @param show */ public void showBalance(boolean show) { if (show) { balanceColumn.getColumn().setWidth(70); } else { balanceColumn.getColumn().setWidth(0); } } /** * Returns the last statement in the list of statements for * the account. This is used to determine default values when * the user creates a new statement and also the starting * balance of any newly created statement. * * @return the last statement, or null if no statements have * yet been created in the account */ public StatementDetails getLastStatement() { return contentProvider.getLastStatement(); } /** * @return the last statement before the given statement, or null if there is no * statement prior to the given statement */ public BankStatement getPriorStatement(BankStatement statement) { return contentProvider.getPriorStatement(statement); } /** * Listen for changes in the selected statement. */ public void addSelectionListener(SelectionAdapter listener) { // Pass thru the request to the table control. statementTable.addSelectionListener(listener); } /** * Returns the opening balance for a given statement. * <P> * The statement may not yet exist, so no entry for this * statement will yet be in the statement map. The statement * will not necessarily be the last statement because the user * may be working backwards and entering prior statements. * * @param statement * @return the opening balance of this statement */ public long getStatementOpeningBalance(BankStatement statement) { return contentProvider.getStatementOpeningBalance(statement); } }