/**
* Copyright (c) 2012, Lindsay Bradford and other Contributors.
* All rights reserved.
*
* This program and the accompanying materials are made available
* under the terms of the BSD 3-Clause licence which accompanies
* this distribution, and is available at
* http://opensource.org/licenses/BSD-3-Clause
*/
package blacksmyth.personalfinancier.model.budget;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Observable;
import java.util.Observer;
import blacksmyth.general.ReflectionUtilities;
import blacksmyth.general.SortedArrayList;
import blacksmyth.general.file.IFileHandlerModel;
import blacksmyth.personalfinancier.control.FinancierUndoManager;
import blacksmyth.personalfinancier.control.budget.IBudgetController;
import blacksmyth.personalfinancier.control.budget.IBudgetObserver;
import blacksmyth.personalfinancier.model.Account;
import blacksmyth.personalfinancier.model.AccountModel;
import blacksmyth.personalfinancier.model.CashFlowFrequency;
import blacksmyth.personalfinancier.model.Money;
import blacksmyth.personalfinancier.model.MoneyFactory;
import blacksmyth.personalfinancier.model.CashFlowFrequencyUtility;
import blacksmyth.personalfinancier.model.ModelPreferences;
public class BudgetModel extends Observable implements Observer, IBudgetController,
IFileHandlerModel<BudgetFileContent> {
private static final String CONTROLLER_ASSERT_MSG = "Caller does not implement IBudgetController.";
private static final String VIEWER_ASSERT_MSG = "Caller does not implement IBudgetObserver.";
private final FinancierUndoManager undoManager = new FinancierUndoManager();
private AccountModel accountModel;
private HashSet<String> expenseCategories;
private HashSet<String> incomeCategories;
private ArrayList<BudgetItem> expenseItems;
private ArrayList<BudgetItem> incomeItems;
private SortedArrayList<AccountSummary> cashFlowSummaries;
private SortedArrayList<CategorySummary> categorySummaries;
private Money netCashFlow;
public BudgetModel(AccountModel accountModel) {
this.expenseCategories = new HashSet<String>();
this.incomeCategories = new HashSet<String>();
this.expenseItems = new ArrayList<BudgetItem>();
this.incomeItems = new ArrayList<BudgetItem>();
this.cashFlowSummaries = new SortedArrayList<AccountSummary>();
this.categorySummaries = new SortedArrayList<CategorySummary>();
this.accountModel = accountModel;
this.accountModel.addObserver(this);
this.changeAndNotifyObservers();
}
public AccountModel getAccountModel() {
return this.accountModel;
}
public FinancierUndoManager getUndoManager() {
return undoManager;
}
public BudgetModel(BudgetFileContent state) {
this.expenseCategories = state.expenseCategories;
this.incomeCategories = state.incomeCategories;
this.expenseItems = state.expenseItems;
this.incomeItems = state.incomeItems;
this.accountModel = new AccountModel();
this.changeAndNotifyObservers();
}
public BudgetItem addExpenseItem() {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
BudgetItem newItem = BudgetItemFactory.createExpense();
this.addExpenseItem(newItem);
return newItem;
}
public BudgetItem addExpenseItem(int itemIndex) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
BudgetItem newItem = BudgetItemFactory.createExpense();
if (itemIndex - 1 >= 0) {
BudgetItem itemAtIndex = this.getExpenseItems().get(itemIndex - 1);
newItem.setBudgetAccount(
itemAtIndex.getBudgetAccount()
);
newItem.setCategory(
itemAtIndex.getCategory()
);
newItem.setFrequency(
itemAtIndex.getFrequency()
);
}
this.addExpenseItem(itemIndex, newItem);
return newItem;
}
public BudgetItem addIncomeItem() {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
BudgetItem newItem = BudgetItemFactory.createIncome();
this.addIncomeItem(newItem);
return newItem;
}
public BudgetItem addIncomeItem(int itemIndex) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
BudgetItem newItem = BudgetItemFactory.createIncome();
if (itemIndex - 1 >= 0) {
BudgetItem itemAtIndex = this.getIncomeItems().get(itemIndex - 1);
newItem.setBudgetAccount(
itemAtIndex.getBudgetAccount()
);
newItem.setCategory(
itemAtIndex.getCategory()
);
newItem.setFrequency(
itemAtIndex.getFrequency()
);
}
this.addIncomeItem(itemIndex, newItem);
return newItem;
}
public void addExpenseItem(BudgetItem item) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.expenseItems.add(item);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public void addIncomeItem(BudgetItem item) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.incomeItems.add(item);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void addIncomeItem(int index, BudgetItem item) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.incomeItems.add(index, item);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void addExpenseItem(int index, BudgetItem item) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.expenseItems.add(index, item);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public BudgetItem removeExpenseItem(int index) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
BudgetItem removedItem = this.expenseItems.remove(index);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
return removedItem;
}
public BudgetItem removeIncomeItem(int index) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
BudgetItem removedItem = this.incomeItems.remove(index);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
return removedItem;
}
public void removeIncomeItem(BudgetItem item) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.incomeItems.remove(item);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void removeExpenseItem(BudgetItem item) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.expenseItems.remove(item);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public ArrayList<BudgetItem> getExpenseItems() {
return this.expenseItems;
}
public void setExpenseItems(ArrayList<BudgetItem> expenseItems) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.expenseItems = expenseItems;
this.changeAndNotifyObservers();
}
public ArrayList<BudgetItem> getIncomeItems() {
return incomeItems;
}
public void setIncomeItems(ArrayList<BudgetItem> incomeItems) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.incomeItems = incomeItems;
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void setExpenseItemDescription(int index, String newDescription) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.expenseItems.size());
this.expenseItems.get(index).setDescription(newDescription);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public void setIncomeItemDescription(int index, String newDescription) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.incomeItems.size());
this.incomeItems.get(index).setDescription(newDescription);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void setExpenseItemTotal(int index, BigDecimal total) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.expenseItems.size());
this.expenseItems.get(index).getBudgettedAmount().setTotal(total);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public void setIncomeItemTotal(int index, BigDecimal total) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.incomeItems.size());
this.incomeItems.get(index).getBudgettedAmount().setTotal(total);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void setExpenseItemFrequency(int index, CashFlowFrequency frequency) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.expenseItems.size());
this.expenseItems.get(index).setFrequency(frequency);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public void setIncomeItemFrequency(int index, CashFlowFrequency frequency) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.incomeItems.size());
this.incomeItems.get(index).setFrequency(frequency);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void setExpenseItemAccount(int index, String accountName) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.expenseItems.size());
this.expenseItems.get(index).setBudgetAccount(
accountModel.getAccount(accountName)
);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public void setIncomeItemAccount(int index, String accountName) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.incomeItems.size());
this.incomeItems.get(index).setBudgetAccount(
accountModel.getAccount(accountName)
);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void setExpenseItemCategory(int index, String category) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.expenseItems.size());
this.expenseItems.get(index).setCategory(category);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public void setIncomeItemCategory(int index, String category) {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
assert (index >= 0 && index < this.incomeItems.size());
this.incomeItems.get(index).setCategory(category);
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public ArrayList<Account> getBudgetAccounts() {
return accountModel.getBudgetAccounts();
}
public Account getBudgetAccount(String nickname) {
return accountModel.getBudgetAccount(nickname);
}
public void removeAllExpenseItems() {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.expenseItems = new ArrayList<BudgetItem>();
this.changeAndNotifyObservers(
BudgetEvent.ItemType.ExpenseItems
);
}
public void removeAllIncomeItems() {
assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
this.incomeItems = new ArrayList<BudgetItem>();
this.changeAndNotifyObservers(
BudgetEvent.ItemType.IncomeItems
);
}
public void removeAllBudgetItmes() {
this.removeAllIncomeItems();
this.removeAllExpenseItems();
}
public HashSet<String> getExpenseCategories() {
return expenseCategories;
}
public void setExpenseCategories(HashSet<String> expenseCategories) {
this.expenseCategories = expenseCategories;
this.changeAndNotifyObservers(
BudgetEvent.ItemType.expenseCategories
);
}
public void addExpenseCategory(String newCategory) {
if (this.expenseCategories.add(newCategory)) {
this.changeAndNotifyObservers(
BudgetEvent.ItemType.expenseCategories
);
}
}
public HashSet<String> getIncomeCategories() {
return incomeCategories;
}
public void setIncomeCategories(HashSet<String> incomeCategories) {
this.incomeCategories = incomeCategories;
this.changeAndNotifyObservers(
BudgetEvent.ItemType.incomeCategories
);
}
public void addIncomeCategory(String newCategory) {
System.out.println("Adding Income Category: " + newCategory);
if (this.incomeCategories.add(newCategory)) {
this.changeAndNotifyObservers(
BudgetEvent.ItemType.incomeCategories
);
}
}
public void moveExpenseItemDown(int itemIndex) {
BudgetItem movingExpense = this.removeExpenseItem(itemIndex);
this.addExpenseItem(itemIndex + 1, movingExpense);
}
public void moveExpenseItemUp(int itemIndex) {
BudgetItem movingExpense = this.removeExpenseItem(itemIndex);
this.addExpenseItem(itemIndex - 1, movingExpense);
}
public void moveIncomeItemDown(int itemIndex) {
BudgetItem movingIncome = this.removeIncomeItem(itemIndex);
this.addIncomeItem(itemIndex + 1, movingIncome);
}
public void moveIncomeItemUp(int itemIndex) {
BudgetItem movingIncome = this.removeIncomeItem(itemIndex);
this.addIncomeItem(itemIndex - 1, movingIncome);
}
public Money getNetCashFlow() {
return this.netCashFlow;
}
public ArrayList<AccountSummary> getCashFlowSummaries() {
return this.cashFlowSummaries;
}
public SortedArrayList<CategorySummary> getCategorySummaries() {
return this.categorySummaries;
}
private void updateDerivedData() {
updateCashFlowSummaries();
updateCategorySummaries();
updateNetCashFlow();
}
private void updateNetCashFlow() {
netCashFlow = MoneyFactory.createAmount(0);
for(AccountSummary summary : this.cashFlowSummaries) {
netCashFlow.setTotal(
netCashFlow.getTotal().add(
summary.getBudgettedAmount().getTotal()
)
);
}
}
private void updateCashFlowSummaries() {
Hashtable<String, AccountSummary> summaryTable = new Hashtable<String, AccountSummary>();
this.cashFlowSummaries = new SortedArrayList<AccountSummary>();
netCashFlow = MoneyFactory.createAmount(0);
for(Account account : this.accountModel.getBudgetAccounts()) {
AccountSummary newSummary = new AccountSummary(account);
summaryTable.put(account.getNickname(), newSummary);
}
for(BudgetItem item : this.incomeItems) {
AccountSummary summary = summaryTable.get(item.getBudgetAccount().getNickname());
BigDecimal convertedBudgetAmount = CashFlowFrequencyUtility.convertFrequencyAmount(
item.getBudgettedAmount().getTotal(),
item.getFrequency(),
summary.getBudgettedFrequency()
);
BigDecimal originalTotal = summary.getBudgettedAmount().getTotal();
BigDecimal newTotal = originalTotal.add(
convertedBudgetAmount,
ModelPreferences.getInstance().getPreferredMathContext()
);
summary.getBudgettedAmount().setTotal(newTotal);
}
for(BudgetItem item : this.expenseItems) {
AccountSummary summary = summaryTable.get(item.getBudgetAccount().getNickname());
BigDecimal convertedBudgetAmount = CashFlowFrequencyUtility.convertFrequencyAmount(
item.getBudgettedAmount().getTotal(),
item.getFrequency(),
summary.getBudgettedFrequency()
);
BigDecimal originalTotal = summary.getBudgettedAmount().getTotal();
BigDecimal newTotal = originalTotal.subtract(
convertedBudgetAmount,
ModelPreferences.getInstance().getPreferredMathContext()
);
summary.getBudgettedAmount().setTotal(newTotal);
}
for(AccountSummary summary : summaryTable.values()) {
this.cashFlowSummaries.insertSorted(summary);
}
}
private void updateCategorySummaries() {
Hashtable<String, CategorySummary> summaryTable = new Hashtable<String, CategorySummary>();
this.categorySummaries = new SortedArrayList<CategorySummary>();
for(BudgetItem item : this.incomeItems) {
if (!summaryTable.containsKey(item.getCategory().toString())) {
CategorySummary newSummary = new CategorySummary(item.getCategory().toString());
summaryTable.put(item.getCategory().toString(), newSummary);
}
CategorySummary summary = summaryTable.get(item.getCategory().toString());
BigDecimal convertedBudgetAmount = CashFlowFrequencyUtility.convertFrequencyAmount(
item.getBudgettedAmount().getTotal(),
item.getFrequency(),
summary.getBudgettedFrequency()
);
BigDecimal originalTotal = summary.getBudgettedAmount().getTotal();
BigDecimal newTotal = originalTotal.add(
convertedBudgetAmount,
ModelPreferences.getInstance().getPreferredMathContext()
);
summary.getBudgettedAmount().setTotal(newTotal);
}
for(BudgetItem item : this.expenseItems) {
if (!summaryTable.containsKey(item.getCategory().toString())) {
CategorySummary newSummary = new CategorySummary(item.getCategory().toString());
summaryTable.put(item.getCategory().toString(), newSummary);
}
CategorySummary summary = summaryTable.get(item.getCategory().toString());
BigDecimal convertedBudgetAmount = CashFlowFrequencyUtility.convertFrequencyAmount(
item.getBudgettedAmount().getTotal(),
item.getFrequency(),
summary.getBudgettedFrequency()
);
BigDecimal originalTotal = summary.getBudgettedAmount().getTotal();
BigDecimal newTotal = originalTotal.subtract(
convertedBudgetAmount,
ModelPreferences.getInstance().getPreferredMathContext()
);
summary.getBudgettedAmount().setTotal(newTotal);
}
for (CategorySummary summary : summaryTable.values()) {
this.categorySummaries.insertSorted(summary);
}
}
public void changeAndNotifyObservers() {
changeAndNotifyObservers(
BudgetEvent.ItemType.AllItems
);
}
public void changeAndNotifyObservers(BudgetEvent.ItemType itemType) {
this.setChanged();
this.updateDerivedData();
this.notifyObservers(
new BudgetEvent(
itemType
)
);
}
public void addObserver(Observer observer) {
assert (ReflectionUtilities.classImplements(observer.getClass(), IBudgetObserver.class)) : VIEWER_ASSERT_MSG;
super.addObserver(observer);
this.changeAndNotifyObservers();
}
public void update(Observable o, Object arg) {
this.changeAndNotifyObservers();
}
public BudgetFileContent toSerializable() {
BudgetFileContent state = new BudgetFileContent();
state.incomeCategories = this.incomeCategories;
state.expenseCategories = this.expenseCategories;
state.accounts = this.accountModel.getAccounts();
state.incomeItems = this.incomeItems;
state.expenseItems = this.expenseItems;
return state;
}
public void fromSerializable(BudgetFileContent state) {
// assert ReflectionUtilities.callerImplements(IBudgetController.class) : CONTROLLER_ASSERT_MSG;
if (state == null) {
return;
}
this.incomeCategories = state.incomeCategories;
this.expenseCategories = state.expenseCategories;
this.accountModel.setAccounts(state.accounts);
this.incomeItems = state.incomeItems;
this.expenseItems = state.expenseItems;
this.changeAndNotifyObservers();
}
}