001: /*
002: *
003: * JMoney - A Personal Finance Manager
004: * Copyright (c) 2004 Nigel Westbury <westbury@users.sourceforge.net>
005: *
006: *
007: * This program is free software; you can redistribute it and/or modify
008: * it under the terms of the GNU General Public License as published by
009: * the Free Software Foundation; either version 2 of the License, or
010: * (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
020: *
021: */
022:
023: package net.sf.jmoney.reconciliation.reconcilePage;
024:
025: import java.util.Map;
026: import java.util.SortedMap;
027: import java.util.TreeMap;
028:
029: import net.sf.jmoney.model2.Currency;
030: import net.sf.jmoney.model2.CurrencyAccount;
031: import net.sf.jmoney.model2.Entry;
032: import net.sf.jmoney.model2.EntryInfo;
033: import net.sf.jmoney.model2.ExtendableObject;
034: import net.sf.jmoney.model2.ScalarPropertyAccessor;
035: import net.sf.jmoney.model2.SessionChangeAdapter;
036: import net.sf.jmoney.reconciliation.BankStatement;
037: import net.sf.jmoney.reconciliation.IReconciliationQueries;
038: import net.sf.jmoney.reconciliation.ReconciliationEntryInfo;
039:
040: import org.eclipse.jface.viewers.ColumnLabelProvider;
041: import org.eclipse.jface.viewers.IStructuredContentProvider;
042: import org.eclipse.jface.viewers.TableViewer;
043: import org.eclipse.jface.viewers.TableViewerColumn;
044: import org.eclipse.jface.viewers.Viewer;
045: import org.eclipse.jface.viewers.ViewerComparator;
046: import org.eclipse.swt.SWT;
047: import org.eclipse.swt.events.SelectionAdapter;
048: import org.eclipse.swt.layout.GridData;
049: import org.eclipse.swt.layout.GridLayout;
050: import org.eclipse.swt.widgets.Composite;
051: import org.eclipse.swt.widgets.Table;
052: import org.eclipse.ui.forms.SectionPart;
053: import org.eclipse.ui.forms.widgets.FormToolkit;
054: import org.eclipse.ui.forms.widgets.Section;
055:
056: /**
057: * Implementation of the 'Statements' section of the reconciliation
058: * page. This section lists all the statements in the account that
059: * have reconciled entries in them.
060: *
061: * @author Nigel Westbury
062: */
063: public class StatementsSection extends SectionPart {
064:
065: protected Table statementTable;
066:
067: protected StatementContentProvider contentProvider;
068:
069: private TableViewerColumn balanceColumn;
070:
071: public StatementsSection(Composite parent, FormToolkit toolkit,
072: CurrencyAccount account) {
073: super (parent, toolkit, Section.DESCRIPTION | Section.TITLE_BAR);
074: getSection().setText("Statements");
075: getSection().setDescription(
076: "Double click a statement to show that statement.");
077:
078: final Currency currencyForFormatting = account.getCurrency();
079:
080: Composite composite = new Composite(getSection(), SWT.NONE);
081: composite.setLayout(new GridLayout());
082:
083: statementTable = new Table(composite, SWT.FULL_SELECTION
084: | SWT.SINGLE | SWT.V_SCROLL);
085: GridData gdTable = new GridData(SWT.FILL, SWT.FILL, true, true);
086: gdTable.heightHint = 100;
087: statementTable.setLayoutData(gdTable);
088:
089: statementTable.setHeaderVisible(true);
090: statementTable.setLinesVisible(true);
091:
092: // Create and setup the TableViewer
093: TableViewer tableViewer = new TableViewer(statementTable);
094: tableViewer.setUseHashlookup(true);
095:
096: // Add the columns
097: TableViewerColumn statementColumn = new TableViewerColumn(
098: tableViewer, SWT.LEFT);
099: statementColumn.getColumn().setWidth(65);
100: statementColumn.getColumn().setText("Statement");
101: statementColumn.setLabelProvider(new ColumnLabelProvider() {
102: @Override
103: public String getText(Object element) {
104: StatementDetails statementDetails = (StatementDetails) element;
105: return statementDetails.statement.toLocalizedString();
106: }
107: });
108:
109: balanceColumn = new TableViewerColumn(tableViewer, SWT.RIGHT);
110: balanceColumn.getColumn().setWidth(70);
111: balanceColumn.getColumn().setText("Balance");
112: balanceColumn.setLabelProvider(new ColumnLabelProvider() {
113: @Override
114: public String getText(Object element) {
115: StatementDetails statementDetails = (StatementDetails) element;
116: return currencyForFormatting.format(statementDetails
117: .getClosingBalance());
118: }
119: });
120:
121: contentProvider = new StatementContentProvider(tableViewer);
122:
123: tableViewer.setContentProvider(contentProvider);
124: tableViewer.setComparator(new StatementViewerComparator());
125: tableViewer.setInput(account);
126:
127: /*
128: * Scroll the statement list to the bottom so that the most recent
129: * statements are shown (if there are any statements).
130: */
131: StatementDetails lastStatementDetails = contentProvider
132: .getLastStatement();
133: if (lastStatementDetails != null) {
134: tableViewer.reveal(lastStatementDetails);
135: }
136:
137: getSection().setClient(composite);
138: toolkit.paintBordersFor(composite);
139: refresh();
140: }
141:
142: class StatementContentProvider implements
143: IStructuredContentProvider {
144: /**
145: * The table viewer to be notified whenever the content changes.
146: */
147: TableViewer tableViewer;
148:
149: CurrencyAccount account;
150:
151: SortedMap<BankStatement, StatementDetails> statementDetailsMap;
152:
153: StatementContentProvider(TableViewer tableViewer) {
154: this .tableViewer = tableViewer;
155: }
156:
157: public void inputChanged(Viewer v, Object oldInput,
158: Object newInput) {
159: account = (CurrencyAccount) newInput;
160:
161: // Build a tree map of the statement totals
162:
163: // We use a tree map in preference to the more efficient
164: // hash map because we can then fetch the results in order.
165: statementDetailsMap = new TreeMap<BankStatement, StatementDetails>();
166:
167: // When this item is disposed, the input may be set to null.
168: // Return an empty list in this case.
169: if (newInput == null) {
170: return;
171: }
172:
173: IReconciliationQueries queries = (IReconciliationQueries) account
174: .getSession().getAdapter(
175: IReconciliationQueries.class);
176: if (queries != null) {
177: // TODO: change this method
178: //return queries.getStatements(fPage.getAccount());
179: } else {
180: // IReconciliationQueries has not been implemented in the datastore.
181: // We must therefore provide our own implementation.
182:
183: // We use a tree map in preference to the more efficient
184: // hash map because we can then fetch the results in order.
185: SortedMap<BankStatement, Long> statementTotals = new TreeMap<BankStatement, Long>();
186:
187: for (Entry entry : account.getEntries()) {
188: BankStatement statement = entry
189: .getPropertyValue(ReconciliationEntryInfo
190: .getStatementAccessor());
191:
192: if (statement != null) {
193: Long statementTotal = statementTotals
194: .get(statement);
195: if (statementTotal == null) {
196: statementTotal = new Long(0);
197: }
198: statementTotals.put(statement, new Long(
199: statementTotal.longValue()
200: + entry.getAmount()));
201: }
202: }
203:
204: long balance = account.getStartBalance();
205: for (Map.Entry<BankStatement, Long> mapEntry : statementTotals
206: .entrySet()) {
207: BankStatement statement = mapEntry.getKey();
208: long totalEntriesOnStatement = (mapEntry.getValue())
209: .longValue();
210:
211: statementDetailsMap.put(statement,
212: new StatementDetails(statement, balance,
213: totalEntriesOnStatement));
214: balance += totalEntriesOnStatement;
215: }
216: }
217:
218: // Listen for changes so we can keep the tree map up to date.
219: account.getDataManager().addChangeListener(
220: new SessionChangeAdapter() {
221: @Override
222: public void objectCreated(
223: ExtendableObject newObject) {
224: if (newObject instanceof Entry) {
225: Entry newEntry = (Entry) newObject;
226: if (account.equals(newEntry
227: .getAccount())) {
228: BankStatement statement = newEntry
229: .getPropertyValue(ReconciliationEntryInfo
230: .getStatementAccessor());
231: adjustStatement(statement, newEntry
232: .getAmount());
233: }
234: }
235: }
236:
237: @Override
238: public void objectDestroyed(
239: ExtendableObject deletedObject) {
240: if (deletedObject instanceof Entry) {
241: Entry deletedEntry = (Entry) deletedObject;
242: if (account.equals(deletedEntry
243: .getAccount())) {
244: BankStatement statement = deletedEntry
245: .getPropertyValue(ReconciliationEntryInfo
246: .getStatementAccessor());
247: adjustStatement(statement,
248: -deletedEntry.getAmount());
249: }
250: }
251: }
252:
253: @Override
254: public void objectChanged(
255: ExtendableObject changedObject,
256: ScalarPropertyAccessor changedProperty,
257: Object oldValue, Object newValue) {
258: if (changedObject instanceof Entry) {
259: Entry entry = (Entry) changedObject;
260:
261: if (changedProperty == EntryInfo
262: .getAccountAccessor()) {
263: if (account.equals(oldValue)) {
264: BankStatement statement = entry
265: .getPropertyValue(ReconciliationEntryInfo
266: .getStatementAccessor());
267: adjustStatement(statement,
268: -entry.getAmount());
269: }
270: if (account.equals(newValue)) {
271: BankStatement statement = entry
272: .getPropertyValue(ReconciliationEntryInfo
273: .getStatementAccessor());
274: adjustStatement(statement,
275: entry.getAmount());
276: }
277: } else {
278: if (account.equals(entry
279: .getAccount())) {
280: if (changedProperty == EntryInfo
281: .getAmountAccessor()) {
282: BankStatement statement = entry
283: .getPropertyValue(ReconciliationEntryInfo
284: .getStatementAccessor());
285: long oldAmount = ((Long) oldValue)
286: .longValue();
287: long newAmount = ((Long) newValue)
288: .longValue();
289: adjustStatement(statement,
290: newAmount
291: - oldAmount);
292: } else if (changedProperty == ReconciliationEntryInfo
293: .getStatementAccessor()) {
294: adjustStatement(
295: (BankStatement) oldValue,
296: -entry.getAmount());
297: adjustStatement(
298: (BankStatement) newValue,
299: entry.getAmount());
300: }
301: }
302: }
303: }
304: }
305: }, statementTable);
306: }
307:
308: /**
309: * @param statement the statement to be adjusted. This parameter
310: * may be null in which case this method does nothing
311: * @param amount the amount by which the total for the given statement
312: * is to be adjusted
313: */
314: void adjustStatement(BankStatement statement, long amount) {
315: if (statement != null) {
316: StatementDetails this StatementDetails = statementDetailsMap
317: .get(statement);
318: if (this StatementDetails == null) {
319:
320: long openingBalance;
321: SortedMap<BankStatement, StatementDetails> priorStatements = statementDetailsMap
322: .headMap(statement);
323: if (priorStatements.isEmpty()) {
324: openingBalance = account.getStartBalance();
325: } else {
326: openingBalance = priorStatements.get(
327: priorStatements.lastKey())
328: .getClosingBalance();
329: }
330:
331: this StatementDetails = new StatementDetails(
332: statement, openingBalance, amount);
333:
334: statementDetailsMap.put(statement,
335: this StatementDetails);
336:
337: // Notify the viewer that we have a new item
338: tableViewer.add(this StatementDetails);
339: } else {
340: this StatementDetails.adjustEntriesTotal(amount);
341:
342: // Notify the viewer that an item has changed
343: tableViewer.update(this StatementDetails, null);
344: }
345:
346: // This total affects all later balances.
347:
348: // Iterate through all later statements updating the
349: // statement details and then notify the viewer of the
350: // update. Note that tailMap returns a collection that
351: // includes the starting key, so we must be sure not to
352: // process that.
353: SortedMap<BankStatement, StatementDetails> laterStatements = statementDetailsMap
354: .tailMap(statement);
355: for (StatementDetails statementDetails : laterStatements
356: .values()) {
357: if (!statementDetails.statement.equals(statement)) {
358: statementDetails.adjustOpeningBalance(amount);
359:
360: // Notify the viewer that an item has changed
361: tableViewer.update(statementDetails, null);
362: }
363: }
364: }
365: }
366:
367: public void dispose() {
368: }
369:
370: public Object[] getElements(Object parent) {
371: // Return an array of the statements. These are already ordered.
372: return statementDetailsMap.values().toArray();
373: }
374:
375: /**
376: * @return
377: */
378: public StatementDetails getLastStatement() {
379: if (this .statementDetailsMap.isEmpty()) {
380: return null;
381: } else {
382: return statementDetailsMap.get(statementDetailsMap
383: .lastKey());
384: }
385: }
386: }
387:
388: /**
389: * Even though the content provider supplies the statements in order, we
390: * still need to set a sorter into the table. The reason is that new
391: * statements will otherwise always be added at the end and it is possible,
392: * though unlikely, that a user will go back and add earlier statements.
393: */
394: class StatementViewerComparator extends ViewerComparator {
395: @Override
396: public int compare(Viewer viewer, Object element1,
397: Object element2) {
398: StatementDetails statementDetails1 = (StatementDetails) element1;
399: StatementDetails statementDetails2 = (StatementDetails) element2;
400: return statementDetails1.compareTo(statementDetails2);
401: }
402: }
403:
404: /**
405: * @param show
406: */
407: public void showBalance(boolean show) {
408: if (show) {
409: balanceColumn.getColumn().setWidth(70);
410: } else {
411: balanceColumn.getColumn().setWidth(0);
412: }
413: }
414:
415: /**
416: * Returns the last statement in the list of statements for
417: * the account. This is used to determine default values when
418: * the user creates a new statement and also the starting
419: * balance of any newly created statement.
420: *
421: * @return the last statement, or null if no statements have
422: * yet been created in the account
423: */
424: public StatementDetails getLastStatement() {
425: return contentProvider.getLastStatement();
426: }
427:
428: /**
429: * Listen for changes in the selected statement.
430: */
431: public void addSelectionListener(SelectionAdapter listener) {
432: // Pass thru the request to the table control.
433: statementTable.addSelectionListener(listener);
434: }
435: }
|