Coverage Report - org.homeunix.thecave.buddi.model.impl.DocumentImpl
 
Classes in this File Line Coverage Branch Coverage Complexity
DocumentImpl
18%
94/512
11%
37/336
4.203
DocumentImpl$1
0%
0/9
0%
0/2
4.203
DocumentImpl$2
0%
0/21
0%
0/2
4.203
DocumentImpl$3
0%
0/3
N/A
4.203
DocumentImpl$4
0%
0/4
N/A
4.203
DocumentImpl$5
0%
0/3
N/A
4.203
DocumentImpl$6
0%
0/3
N/A
4.203
DocumentImpl$7
0%
0/3
N/A
4.203
 
 1  
 /*
 2  
  * Created on Jul 29, 2007 by wyatt
 3  
  */
 4  
 package org.homeunix.thecave.buddi.model.impl;
 5  
 
 6  
 import java.beans.Encoder;
 7  
 import java.beans.ExceptionListener;
 8  
 import java.beans.Expression;
 9  
 import java.beans.PersistenceDelegate;
 10  
 import java.beans.XMLEncoder;
 11  
 import java.io.File;
 12  
 import java.io.FileOutputStream;
 13  
 import java.io.IOException;
 14  
 import java.io.OutputStream;
 15  
 import java.util.ArrayList;
 16  
 import java.util.Calendar;
 17  
 import java.util.Date;
 18  
 import java.util.GregorianCalendar;
 19  
 import java.util.HashMap;
 20  
 import java.util.LinkedList;
 21  
 import java.util.List;
 22  
 import java.util.Map;
 23  
 import java.util.concurrent.Semaphore;
 24  
 import java.util.logging.Level;
 25  
 import java.util.logging.Logger;
 26  
 
 27  
 import javax.swing.JOptionPane;
 28  
 
 29  
 import org.homeunix.thecave.buddi.Const;
 30  
 import org.homeunix.thecave.buddi.i18n.BuddiKeys;
 31  
 import org.homeunix.thecave.buddi.i18n.keys.ButtonKeys;
 32  
 import org.homeunix.thecave.buddi.i18n.keys.ScheduleFrequency;
 33  
 import org.homeunix.thecave.buddi.model.Account;
 34  
 import org.homeunix.thecave.buddi.model.AccountType;
 35  
 import org.homeunix.thecave.buddi.model.BudgetCategory;
 36  
 import org.homeunix.thecave.buddi.model.Document;
 37  
 import org.homeunix.thecave.buddi.model.ModelObject;
 38  
 import org.homeunix.thecave.buddi.model.ScheduledTransaction;
 39  
 import org.homeunix.thecave.buddi.model.Source;
 40  
 import org.homeunix.thecave.buddi.model.Split;
 41  
 import org.homeunix.thecave.buddi.model.Transaction;
 42  
 import org.homeunix.thecave.buddi.model.TransactionSplit;
 43  
 import org.homeunix.thecave.buddi.model.prefs.PrefsModel;
 44  
 import org.homeunix.thecave.buddi.plugin.api.exception.DataModelProblemException;
 45  
 import org.homeunix.thecave.buddi.plugin.api.exception.InvalidValueException;
 46  
 import org.homeunix.thecave.buddi.plugin.api.exception.ModelException;
 47  
 import org.homeunix.thecave.buddi.plugin.api.util.TextFormatter;
 48  
 import org.homeunix.thecave.buddi.util.BuddiCryptoFactory;
 49  
 import org.homeunix.thecave.buddi.util.FileFunctions;
 50  
 import org.homeunix.thecave.buddi.view.dialogs.BuddiPasswordDialog;
 51  
 
 52  
 import ca.digitalcave.moss.application.document.AbstractDocument;
 53  
 import ca.digitalcave.moss.application.document.exception.DocumentSaveException;
 54  
 import ca.digitalcave.moss.collections.CompositeList;
 55  
 import ca.digitalcave.moss.collections.SortedArrayList;
 56  
 import ca.digitalcave.moss.common.DateUtil;
 57  
 import ca.digitalcave.moss.crypto.CipherException;
 58  
 
 59  
 
 60  
 /**
 61  
  * The main container class for the new data model, to be implemented in Buddi version 3.0.
 62  
  * This contains all the data, most of it in list form.  This object is the root of the XML
 63  
  * file as serialized by XMLEncoder.
 64  
  * 
 65  
  * You should *not* create this class by calling its constructor - you must create it using
 66  
  * one of the ModelFactory.createDocument methods.  The factory will correctly initialize
 67  
  * the default types and budget categories.  The only reason we did not make the default
 68  
  * constructor for this class to be non-public was because the XMLDecoder needs public
 69  
  * consructors to create objects at load time.  
 70  
  * 
 71  
  * @author wyatt
 72  
  */
 73  0
 public class DocumentImpl extends AbstractDocument implements ModelObject, Document {
 74  
         //Store the password when loaded, and use the same one for save().
 75  
         // This is obviously not the best practice to use 
 76  
         // from a security point of view.  However, if a malicious
 77  
         // third party has good enough access to the machine to be able
 78  
         // to read private Java objects, they will just read it when
 79  
         // we call MossCryptoFactory anyways - the password is handed 
 80  
         // there in plain text as well.  The window is already there - this
 81  
         // just increases the time it is available for.
 82  
         private char[] password;
 83  
         private int flags;
 84  
 
 85  
         //Convenience class for checking if objects are already entered.
 86  3034
         private final Map<String, ModelObject> uidMap = new HashMap<String, ModelObject>();
 87  
 
 88  
         //User data objects
 89  3034
         private List<Account> accounts = new SortedArrayList<Account>();
 90  3034
         private List<BudgetCategory> budgetCategories = new SortedArrayList<BudgetCategory>();
 91  3034
         private List<AccountType> accountTypes = new SortedArrayList<AccountType>();
 92  3034
         private List<Transaction> transactions = new SortedArrayList<Transaction>();
 93  3034
         private List<ScheduledTransaction> scheduledTransactions = new SortedArrayList<ScheduledTransaction>();
 94  
 
 95  
         //Model object data
 96  
         private Time modifiedTime;
 97  
         private String uid;
 98  
         
 99  3034
         private final Logger logger = Logger.getLogger(DocumentImpl.class.getName());
 100  
 
 101  
         /**
 102  
          * By default, we start with one batch change enabled.  This is because, otherwise,
 103  
          * the XMLDecoder will cause many model change events to be fired, which will result in
 104  
          * much longer load times.  You must call finishBatchChange() before any change 
 105  
          * events will be sent!  ModelFactory does this for you automatically; as such,
 106  
          * you are much better off to use that to create Document objects.
 107  
          * 
 108  
          * (In fact, this constructor would be private, except for XMLDecoder's need for a public
 109  
          * constructor.  Don't use this unless you know exactly what you are doing!)
 110  
          */
 111  3034
         public DocumentImpl() {
 112  3034
                 setMinimumChangeEventPeriod(1000);
 113  3034
                 startBatchChange();
 114  3034
         }
 115  
 
 116  
         private void doBackupDataFile() {
 117  
                 //Make a backup of the file...
 118  
                 //Backup the file, now that we know it is good...
 119  
                 try{
 120  0
                         if (getFile() != null){
 121  
                                 //Use a rotating backup file, of form 'Data.X.buddi'  
 122  
                                 // The one with the smallest number X is the most recent.
 123  0
                                 String fileBase = getFile().getAbsolutePath().replaceAll(Const.DATA_FILE_EXTENSION + "$", "");
 124  0
                                 for (int i = PrefsModel.getInstance().getNumberOfBackups() - 2; i >= 0; i--){
 125  0
                                         File tempBackupDest = new File(fileBase + "_" + (i + 1) + Const.BACKUP_FILE_EXTENSION);
 126  0
                                         File tempBackupSource = new File(fileBase + "_" + i + Const.BACKUP_FILE_EXTENSION);
 127  0
                                         if (tempBackupSource.exists()){
 128  0
                                                 FileFunctions.copyFile(tempBackupSource, tempBackupDest);
 129  0
                                                 logger.finest("Moving " + tempBackupSource + " to " + tempBackupDest);
 130  
                                         }
 131  
                                 }
 132  0
                                 File tempBackupDest = new File(fileBase + "_0" + Const.BACKUP_FILE_EXTENSION);
 133  0
                                 FileFunctions.copyFile(getFile(), tempBackupDest);
 134  0
                                 if (Const.DEVEL) logger.finest("Backing up file to " + tempBackupDest);
 135  
                         }
 136  
                 }
 137  0
                 catch(IOException ioe){
 138  0
                         logger.log(Level.WARNING, "Problem backing up data files", ioe);
 139  
                 }
 140  0
                 catch (RuntimeException re){
 141  0
                         logger.log(Level.SEVERE, "Runtime Exception encountered when backing up data file!", re);
 142  0
                 }
 143  0
         }
 144  
         
 145  
         public List<Account> getAccounts() {
 146  195003
                 checkLists();
 147  195003
                 return accounts;
 148  
         }
 149  
         public void setAccounts(List<Account> accounts) {
 150  0
                 this.accounts = new SortedArrayList<Account>();
 151  0
                 this.accounts.addAll(accounts);
 152  0
         }
 153  
 
 154  
         public List<BudgetCategory> getBudgetCategories() {
 155  82415
                 return budgetCategories;
 156  
         }
 157  
         public void setBudgetCategories(List<BudgetCategory> budgetCategories) {
 158  0
                 this.budgetCategories = new SortedArrayList<BudgetCategory>();
 159  0
                 this.budgetCategories.addAll(budgetCategories);
 160  0
         }
 161  
         public List<ScheduledTransaction> getScheduledTransactions() {
 162  10953
                 checkLists();
 163  10953
                 return scheduledTransactions;
 164  
         }
 165  
         public void setScheduledTransactions(List<ScheduledTransaction> scheduledTransactions) {
 166  0
                 this.scheduledTransactions = new SortedArrayList<ScheduledTransaction>();
 167  0
                 this.scheduledTransactions.addAll(scheduledTransactions);
 168  0
         }
 169  
         public List<Transaction> getTransactions() {
 170  54780
                 checkLists();
 171  54780
                 return transactions;
 172  
         }
 173  
         public void setTransactions(List<Transaction> transactions) {
 174  0
                 this.transactions = new SortedArrayList<Transaction>();
 175  0
                 this.transactions.addAll(transactions);
 176  0
         }
 177  
         public List<AccountType> getAccountTypes() {
 178  64100
                 checkLists();
 179  64100
                 return accountTypes;
 180  
         }
 181  
         public void setAccountTypes(List<AccountType> types) {
 182  0
                 this.accountTypes = new SortedArrayList<AccountType>();
 183  0
                 this.accountTypes.addAll(types);
 184  0
         }
 185  
 
 186  
         public void setFlag(int flag, boolean set) {
 187  1
                 if (set)
 188  1
                         this.flags |= flag;
 189  
                 else
 190  0
                         this.flags = this.flags & ~flag;
 191  1
         }
 192  
 
 193  
         public void addAccount(Account account) throws ModelException {
 194  0
                 account.setDocument(this);
 195  0
                 checkValid(account, true, false);
 196  0
                 accounts.add(account);
 197  
 //                Collections.sort(accounts);
 198  0
                 setChanged();
 199  0
         }
 200  
         public void addAccountType(AccountType type) throws ModelException {
 201  27306
                 type.setDocument(this);
 202  27306
                 checkValid(type, true, false);
 203  27306
                 accountTypes.add(type);
 204  
 //                Collections.sort(budgetCategories);
 205  27306
                 setChanged();
 206  27306
         }
 207  
         public void addBudgetCategory(BudgetCategory budgetCategory) throws ModelException {
 208  30340
                 budgetCategory.setDocument(this);
 209  30340
                 checkValid(budgetCategory, true, false);
 210  30340
                 budgetCategories.add(budgetCategory);
 211  
 //                Collections.sort(budgetCategories);
 212  30340
                 setChanged();
 213  30340
         }
 214  
         public void addScheduledTransaction(ScheduledTransaction scheduledTransaction) throws ModelException {
 215  0
                 scheduledTransaction.setDocument(this);
 216  0
                 checkValid(scheduledTransaction, true, false);
 217  0
                 scheduledTransactions.add(scheduledTransaction);
 218  
 //                Collections.sort(scheduledTransactions);
 219  0
                 setChanged();
 220  0
         }
 221  
         public void addTransaction(Transaction transaction) throws ModelException {
 222  0
                 transaction.setDocument(this);
 223  0
                 checkValid(transaction, true, false);
 224  0
                 transactions.add(transaction);
 225  
 //                Collections.sort(transactions);
 226  0
                 setChanged();
 227  0
         }
 228  
         public Account getAccount(String name) {
 229  0
                 for (Account a : getAccounts()) { //Try strict matching first
 230  0
                         if (a.getName().equals(name))
 231  0
                                 return a;
 232  
                 }
 233  0
                 for (Account a : getAccounts()) { //Next try to relax it, to ignore case
 234  0
                         if (a.getName().equalsIgnoreCase(name))
 235  0
                                 return a;
 236  
                 }
 237  0
                 for (Account a : getAccounts()) { //Finally try checking full name
 238  0
                         if (a.getFullName().equalsIgnoreCase(name))
 239  0
                                 return a;
 240  
                 }
 241  
 
 242  0
                 return null;
 243  
         }
 244  
         public AccountType getAccountType(String name) {
 245  0
                 for (AccountType at : getAccountTypes()) {
 246  0
                         if (at.getName().equals(name))
 247  0
                                 return at;
 248  
                 }
 249  0
                 for (AccountType at : getAccountTypes()) {
 250  0
                         if (at.getName().equalsIgnoreCase(name))
 251  0
                                 return at;
 252  
                 }
 253  0
                 return null;
 254  
         }
 255  
         public BudgetCategory getBudgetCategory(String fullName) {
 256  0
                 for (BudgetCategory bc : getBudgetCategories()) {
 257  0
                         if (bc.getFullName().equals(fullName))
 258  0
                                 return bc;
 259  
                 }
 260  0
                 for (BudgetCategory bc : getBudgetCategories()) {
 261  0
                         if (bc.getFullName().equalsIgnoreCase(fullName))
 262  0
                                 return bc;
 263  
                 }
 264  0
                 return null;
 265  
         }
 266  
         public ModelObject getObjectByUid(String uid) {
 267  0
                 throw new RuntimeException("Method not implemented");
 268  
         }
 269  
         
 270  3034
         private List<Source> sources = null;
 271  
         @SuppressWarnings("unchecked")
 272  
         public List<Source> getSources() {
 273  0
                 if (sources == null)
 274  0
                         sources = new CompositeList<Source>(true, false, getAccounts(), getBudgetCategories());
 275  0
                 return sources;
 276  
         }
 277  
         public List<Transaction> getTransactions(Date startDate, Date endDate) {
 278  0
                 return new FilteredLists.TransactionListFilteredByDate(this, getTransactions(), startDate, endDate);
 279  
         }
 280  
         public List<Transaction> getTransactions(Source source, Date startDate, Date endDate) {
 281  19140
                 return new FilteredLists.TransactionListFilteredBySource(
 282  
                                 this,
 283  
                                 new FilteredLists.TransactionListFilteredByDate(this, getTransactions(), startDate, endDate),
 284  
                                 source);
 285  
         }
 286  
         public List<Transaction> getTransactions(Source source) {
 287  0
                 return new FilteredLists.TransactionListFilteredBySource(this, getTransactions(), source);
 288  
         }
 289  
         public void removeAccount(Account account) throws ModelException {
 290  0
                 if (getTransactions(account).size() > 0)
 291  0
                         throw new ModelException("Cannot remove account " + account + "; it contains transactions");
 292  0
                 for (ScheduledTransaction st : getScheduledTransactions())
 293  0
                         if (st.getFrom().equals(account)
 294  
                                         || st.getTo().equals(account))
 295  0
                                 throw new ModelException("Cannot remove account " + account + "; it contains scheduled transactions");                
 296  0
                 accounts.remove(account);
 297  0
                 setChanged();
 298  0
         }
 299  
         public void removeAccountType(AccountType type) throws ModelException {
 300  0
                 for (Account a : getAccounts()) {
 301  0
                         if (a.getAccountType().equals(type))
 302  0
                                 throw new ModelException("Cannot remove account type " + type + "; it is referred to by " + a);
 303  
                 }
 304  0
                 accountTypes.remove(type);
 305  0
                 setChanged();
 306  0
         }
 307  
 
 308  
         public void removeBudgetCategory(BudgetCategory budgetCategory) throws ModelException {
 309  
                 //We call the recursive check to ensure that all descendants of this budget category 
 310  
                 // are cleared to be deleted.  If one is not, we cancel the operation.
 311  0
                 recursiveCheckRemoveBudgetCategory(budgetCategory);
 312  
                 
 313  0
                 budgetCategories.remove(budgetCategory);
 314  0
                 setChanged();
 315  0
         }
 316  
         
 317  
         private void recursiveCheckRemoveBudgetCategory(BudgetCategory budgetCategory) throws ModelException {
 318  0
                 if (getTransactions(budgetCategory).size() > 0)
 319  0
                         throw new ModelException("Cannot remove budget category " + budgetCategory + "; it is referenced by at least one transaction");
 320  0
                 for (ScheduledTransaction st : getScheduledTransactions())
 321  0
                         if (st.getFrom().equals(budgetCategory)
 322  
                                         || st.getTo().equals(budgetCategory))
 323  0
                                 throw new ModelException("Cannot remove budget category " + budgetCategory + "; it contains scheduled transactions");                
 324  
                 
 325  0
                 for (BudgetCategory bc : budgetCategory.getChildren()) {
 326  0
                         recursiveCheckRemoveBudgetCategory(bc);
 327  
                 }
 328  0
         }
 329  
         
 330  
         public void removeScheduledTransaction(ScheduledTransaction scheduledTransaction) throws ModelException {
 331  0
                 scheduledTransactions.remove(scheduledTransaction);
 332  0
                 setChanged();
 333  0
         }
 334  
         public void removeTransaction(Transaction transaction) throws ModelException {
 335  0
                 transactions.remove(transaction);
 336  0
                 setChanged();
 337  0
         }
 338  
         /**
 339  
          * Saves the data file to the current file.  If the file has not yet been set,
 340  
          * this method silently returns.
 341  
          * @throws SaveModelException
 342  
          */
 343  
         public void save() throws DocumentSaveException {
 344  0
                 saveWrapper(getFile(), false);
 345  0
         }
 346  
 
 347  
         /**
 348  
          * Saves the data file to the specified file.  If the specified file is 
 349  
          * null, silently return without error and without saving.
 350  
          * @param file
 351  
          * @param flags.  Flags to set for saving.  AND together for multiple flags.
 352  
          * @throws SaveModelException
 353  
          */
 354  
         public void saveAs(File file) throws DocumentSaveException {
 355  0
                 saveWrapper(file, true);
 356  0
         }
 357  
 
 358  
         /**
 359  
          * Saves the autosave file, bypassing backups, etc.
 360  
          * @param file
 361  
          * @throws DocumentSaveException
 362  
          */
 363  3034
         private final Semaphore autosaveMutex = new Semaphore(1);
 364  
         public void saveAuto(File file) throws DocumentSaveException {
 365  
                 //Save the file
 366  
                 try {
 367  0
                         if (!isBatchChange()){
 368  0
                                 BuddiCryptoFactory factory = new BuddiCryptoFactory();
 369  
                                 
 370  0
                                 final OutputStream os = factory.getEncryptedStream(new FileOutputStream(file), password);
 371  
 
 372  
                                 //Clone the file.  This is to decrease the time needed to save large files.
 373  0
                                 final Document clone = clone();
 374  
                                 
 375  0
                                 new Thread(new Runnable(){
 376  
                                         public void run() {
 377  0
                                                 if (autosaveMutex.tryAcquire()){
 378  
                                                         try {
 379  0
                                                                 clone.saveToStream(os);
 380  0
                                                         } catch (DocumentSaveException e) {
 381  0
                                                                 logger.log(Level.WARNING, "There was an error when autosaving the file.", e);
 382  0
                                                         }
 383  
                                                         
 384  0
                                                         autosaveMutex.release();
 385  
                                                 }
 386  
                                                 else {
 387  0
                                                         logger.warning("Did not autosave, as there is another process already waiting.");
 388  
                                                 }
 389  0
                                         }
 390  
                                 }).start();
 391  
                         }
 392  
                 }
 393  0
                 catch (CipherException ce){
 394  
                         //This means that there is something seriously wrong with the encryption methods.
 395  
                         // Perhaps the user's platform does not support the given methods.
 396  
                         // Notify the user, and cancel the save.
 397  0
                         throw new DocumentSaveException(ce);
 398  
                 }
 399  0
                 catch (IOException ioe){
 400  
                         //This means that there was something wrong with the given file, or writing to
 401  
                         // it.  Perhaps the user does not have write access, the folder does not exist,
 402  
                         // or something similar.  Notify the user, and cancel the save.
 403  0
                         throw new DocumentSaveException(ioe);
 404  
                 }
 405  0
                 catch (CloneNotSupportedException cnse){
 406  0
                         logger.warning("There was a problem cloning the data model, prior to auto saving.");
 407  0
                         throw new DocumentSaveException(cnse);
 408  0
                 }
 409  0
         }
 410  
 
 411  
         /**
 412  
          * Wait until any current save operations are completed.
 413  
          * @return
 414  
          * @throws InterruptedException 
 415  
          */
 416  
         public void waitUntilFinishedSaving() throws InterruptedException{
 417  0
                 saveMutex.acquire();
 418  0
                 saveMutex.release();
 419  0
         }
 420  
         
 421  
         public boolean isCurrentlySaving(){
 422  0
                 return saveMutex.availablePermits() == 0;
 423  
         }
 424  
         
 425  
         /**
 426  
          * This is a simple wrapper around the saveInternal method, which adds some key functionality.
 427  
          * This is especially related to handling autoSave files, and will remove the autosave
 428  
          * file associated with this file if it exists. 
 429  
          * @param file
 430  
          * @param flags
 431  
          * @param resetUid
 432  
          * @throws DocumentSaveException
 433  
          */
 434  3034
         private final Semaphore saveMutex = new Semaphore(1);
 435  
         private void saveWrapper(final File file, boolean resetUid) throws DocumentSaveException, ConcurrentSaveException {
 436  0
                 if (!saveMutex.tryAcquire()){
 437  0
                         throw new ConcurrentSaveException("Did not save file, as there is another thread currently saving.");
 438  
                 }
 439  
                 //Do a rotating backup before saving
 440  0
                 doBackupDataFile();
 441  
                 
 442  
                 //Reset the document's UID.  This needs to be done when saving to a different file,
 443  
                 // or else you cannot have multiple files open at the same time.
 444  0
                 if (resetUid)
 445  0
                         setUid(getGeneratedUid(this));
 446  
                 
 447  
                 //Check if we need to reset the password
 448  0
                 if ((flags & RESET_PASSWORD) != 0){
 449  0
                         password = null;
 450  0
                         setFlag(RESET_PASSWORD, false);
 451  
                 }
 452  
 
 453  
                 
 454  
                 //Check if we need to change the password
 455  0
                 if ((flags & CHANGE_PASSWORD) != 0){
 456  0
                         BuddiPasswordDialog passwordDialog = new BuddiPasswordDialog();
 457  0
                         password = passwordDialog.askForPassword(true, false);
 458  0
                         setFlag(CHANGE_PASSWORD, false);
 459  
                 }
 460  
                 
 461  0
                 Thread saveThread = new Thread(new Runnable(){
 462  
                         public void run() {
 463  0
                                 OutputStream os = null;
 464  
                                 try {
 465  
                                         //Clone the file.  This is to decrease the time needed to save large files.
 466  0
                                         Document clone = DocumentImpl.this.clone();
 467  
                                         
 468  
                                         //Save the file
 469  0
                                         BuddiCryptoFactory factory = new BuddiCryptoFactory();
 470  
 //                                        File tempFile = new File(file.getAbsolutePath() + ".temp");
 471  0
                                         os = factory.getEncryptedStream(new FileOutputStream(file), password);
 472  
 
 473  0
                                         clone.saveToStream(os);
 474  
 
 475  
 //                                        //Windows does not support renameTo'ing a file to an existing file.  Thus, we need
 476  
 //                                        // to rename the existing file to a '.old' file, rename the temp file to the
 477  
 //                                        // data file, and remove the .old file if all goes well.  While it would be simpler
 478  
 //                                        // to just remove the data file initially, by renaming it first, we have at least
 479  
 //                                        // some assurance that we are able to recover data if needed.
 480  
 //                                        File oldFile = new File(file.getAbsolutePath() + ".old");
 481  
 //                                        if (oldFile.exists() && !oldFile.delete())
 482  
 //                                                throw new IOException("Unable to delete existing file '" + oldFile + "' in preparation for moving temp file.");
 483  
 //                                        if (file.exists() && !file.renameTo(oldFile))
 484  
 //                                                throw new IOException("Unable to rename existing data file '" + file + "' to '" + oldFile + "'.");
 485  
 //                                        if (!tempFile.renameTo(file))
 486  
 //                                                throw new IOException("Unable to rename temp file '" + tempFile + "' to data file '" + file + "'.");
 487  
 //                                        if (oldFile.exists() && !oldFile.delete())
 488  
 //                                                logger.error("Unable to delete old data file '" + oldFile + "'.  This may cause problems the next time we try to save.");
 489  
                                 }
 490  0
                                 catch (CipherException ce){
 491  
                                         //This means that there is something seriously wrong with the encryption methods.
 492  
                                         // Perhaps the user's platform does not support the given methods.
 493  
                                         // Notify the user, and cancel the save.
 494  0
                                         logger.log(Level.WARNING, "There was a problem related to the encryption of the data file.  Perhaps your Java implemntation does not support the required encryption methods?", ce);
 495  
                                 }
 496  0
                                 catch (IOException ioe){
 497  
                                         //This means that there was something wrong with the given file, or writing to
 498  
                                         // it.  Perhaps the user does not have write access, the folder does not exist,
 499  
                                         // or something similar.  Notify the user, and cancel the save.
 500  0
                                         logger.log(Level.WARNING, "There was an IO error while saving your file.  Please ensure that the desired folder exists, and that you have write access to it.", ioe);
 501  
                                 }
 502  0
                                 catch (DocumentSaveException dse){
 503  0
                                         logger.log(Level.WARNING, "There was a problem saving the document.", dse);
 504  
                                 }
 505  0
                                 catch (CloneNotSupportedException cnse){
 506  0
                                         logger.log(Level.WARNING, "There was a problem cloning the data model, prior to saving.", cnse);
 507  
                                 }
 508  
                                 finally {
 509  0
                                         if (os != null) {
 510  
                                                 try {
 511  0
                                                         os.close();
 512  
                                                 }
 513  0
                                                 catch (IOException ioe) {
 514  0
                                                         logger.log(Level.SEVERE, "Problem encountered while trying to close Output Stream", ioe);
 515  0
                                                 }
 516  
                                         }
 517  
                                 }
 518  
                                 
 519  0
                                 saveMutex.release();
 520  0
                         }
 521  
                 });
 522  
                 
 523  0
                 saveThread.setDaemon(false);
 524  0
                 saveThread.start();
 525  
                 
 526  
                 //Save where we last saved this file.
 527  0
                 setFile(file);
 528  
 //                PrefsModel.getInstance().setLastOpenedDataFile(file);
 529  
 
 530  
                 //Reset the changed flag.
 531  0
                 resetChanged();
 532  
 
 533  
                 //Remove any auto save files which are associated with this document
 534  0
                 File autosave = ModelFactory.getAutoSaveLocation(file);
 535  0
                 if (autosave.exists() && autosave.isFile()){
 536  0
                         if (!autosave.delete())
 537  0
                                 logger.warning("Unable to delete file " + autosave + "; you may be prompted to load this file next time you load.");
 538  
                 }
 539  0
         }
 540  
 
 541  
         /**
 542  
          * Very simple save method.  Streams the document to XML using the 
 543  
          * XMLEncoder, optionally using an encrypted output stream if the password is set.
 544  
          * 
 545  
          * @param file
 546  
          * @param flags
 547  
          * @throws DocumentSaveException
 548  
          */
 549  
         public void saveToStream(OutputStream os) throws DocumentSaveException {
 550  
                 //We don't want to be firing change events in the middle of a save
 551  0
                 startBatchChange();
 552  
 
 553  0
                 XMLEncoder encoder = new XMLEncoder(os);
 554  0
                 encoder.setExceptionListener(new ExceptionListener(){
 555  
                         public void exceptionThrown(Exception e) {
 556  0
                                 logger.log(Level.WARNING, "Error writing XML", e);
 557  0
                         }
 558  
                 });
 559  0
                 encoder.setPersistenceDelegate(File.class, new PersistenceDelegate(){
 560  
                         protected Expression instantiate(Object oldInstance, Encoder out ){
 561  0
                                 File file = (File) oldInstance;
 562  0
                                 String filePath = file.getAbsolutePath();
 563  0
                                 return new Expression(file, file.getClass(), "new", new Object[]{filePath} );
 564  
                         }
 565  
                 });
 566  0
                 encoder.setPersistenceDelegate(Date.class, new PersistenceDelegate(){
 567  
                         @Override
 568  
                         protected Expression instantiate(Object oldInstance, Encoder out) {
 569  0
                                 Date date = (Date) oldInstance;
 570  0
                                 return new Expression(
 571  
                                                 date,
 572  
                                                 DateUtil.class, 
 573  
                                                 "getDate",
 574  
                                                 new Object[]{DateUtil.getYear(date), DateUtil.getMonth(date), DateUtil.getDay(date)});
 575  
                         }
 576  
                 });
 577  0
                 encoder.setPersistenceDelegate(Day.class, new PersistenceDelegate(){
 578  
                         @Override
 579  
                         protected Expression instantiate(Object oldInstance, Encoder out) {
 580  0
                                 Day date = (Day) oldInstance;
 581  0
                                 return new Expression(
 582  
                                                 date,
 583  
                                                 Day.class,
 584  
                                                 "new",
 585  
                                                 new Object[]{DateUtil.getYear(date), DateUtil.getMonth(date), DateUtil.getDay(date)});
 586  
                         }
 587  
                 });
 588  0
                 encoder.setPersistenceDelegate(Time.class, new PersistenceDelegate(){
 589  
                         @Override
 590  
                         protected Expression instantiate(Object oldInstance, Encoder out) {
 591  0
                                 Time time = (Time) oldInstance;
 592  0
                                 return new Expression(
 593  
                                                 time,
 594  
                                                 Time.class, 
 595  
                                                 "new",
 596  
                                                 new Object[]{time.getTime()});
 597  
                         }
 598  
                 });
 599  
                 
 600  
 
 601  0
                 encoder.writeObject(this);
 602  0
                 encoder.flush();
 603  0
                 encoder.close();
 604  
 
 605  0
                 finishBatchChange();
 606  0
         }
 607  
         /**
 608  
          * Updates the balances of all accounts.  Iterates through all accounts, and
 609  
          * calls the updateBalance() method for each. 
 610  
          */
 611  
         public void updateAllBalances(){
 612  3034
                 this.startBatchChange();
 613  3034
                 for (Account a : getAccounts()) {
 614  0
                         a.updateBalance();
 615  
                 }
 616  3034
                 this.finishBatchChange();
 617  3034
         }
 618  
 
 619  
         public Date getModifiedDate() {
 620  0
                 return modifiedTime;
 621  
         }
 622  
         public void setModified(Date modifiedDate) {
 623  175972
                 this.modifiedTime = new Time(modifiedDate);
 624  175972
         }
 625  
         public void setModified(Time modifiedTime){
 626  0
                 this.modifiedTime = modifiedTime;
 627  0
         }
 628  
         public void setChanged(){
 629  175972
                 setModified(new Date());
 630  175972
                 super.setChanged();
 631  175972
         }
 632  
         public String getUid() {
 633  235463
                 if (uid == null || uid.length() == 0){
 634  3034
                         setUid(getGeneratedUid(this));
 635  
                 }
 636  235463
                 return uid;
 637  
         }
 638  
         public void setUid(String uid) {
 639  3034
                 this.uid = uid;
 640  3034
         }
 641  
         public Document getDocument() {
 642  0
                 return this;
 643  
         }
 644  0
         public void setDocument(Document document) {} //Null implementation - this makes no sense
 645  
 
 646  
         @Override
 647  
         public boolean equals(Object obj) {
 648  115292
                 if (obj instanceof DocumentImpl)
 649  115292
                         return this.getUid().equals(((DocumentImpl) obj).getUid());
 650  0
                 return false;
 651  
         }
 652  
 
 653  
         public int compareTo(ModelObject o) {
 654  0
                 return (getUid().compareTo(o.getUid()));
 655  
         }
 656  
 
 657  
         /**
 658  
          * Goes through all objects in the document and verifies that things are
 659  
          * looking correct.  Returns null if things are good, or a string describing
 660  
          * the problems (and optionally steps to resolve) otherwise. 
 661  
          */
 662  
         public String doSanityChecks(){
 663  3034
                 StringBuilder sb = new StringBuilder();
 664  3034
                 Logger logger = Logger.getLogger(this.getClass().getName());
 665  
 
 666  3034
                 List<ScheduledTransaction> stToDelete = new ArrayList<ScheduledTransaction>();
 667  3034
                 for (ScheduledTransaction st : getScheduledTransactions()){
 668  0
                         if (st.getTo() == null
 669  
                                         || st.getFrom() == null){
 670  
                         
 671  0
                                 String message = "The scheduled transaction " + st.getDescription() + " for amount " + st.getAmount() + " doesn't have the To and From sources set up correctly; deleting scheduled transaction.";
 672  0
                                 logger.warning(message);
 673  0
                                 sb.append(message).append("\n\n");
 674  0
                                 stToDelete.add(st);
 675  0
                         }
 676  
                 }
 677  3034
                 for (ScheduledTransaction st : stToDelete) {
 678  
                         try {
 679  0
                                 removeScheduledTransaction(st);
 680  
                         }
 681  0
                         catch (ModelException me){
 682  0
                                 String message = "Unable to delete scheduled transaction " + st.getDescription();
 683  0
                                 logger.severe(message);
 684  0
                                 sb.append(message).append("\n\n");
 685  0
                         }
 686  
                 }
 687  
                 
 688  3034
                 for (Transaction t : getTransactions()) {
 689  
                         try {
 690  0
                                 if (t.getFrom() == null){
 691  0
                                         Source s = null;
 692  0
                                         if (getAccounts().size() > 0)
 693  0
                                                 s = getAccounts().get(0);
 694  0
                                         else if (getBudgetCategories().size() > 0)
 695  0
                                                 s = getBudgetCategories().get(0);
 696  
                                         else
 697  0
                                                 s = null;
 698  0
                                         String message = "Transaction with description '" + t.getDescription() + "' of amount '"+ t.getAmount() + "' on date '" + t.getDate() + "' does not have a From source defined.  Setting this to '" + s.getFullName();
 699  0
                                         logger.warning(message);
 700  0
                                         sb.append(message).append("\n\n");
 701  0
                                         t.setFrom(s);
 702  
                                 }
 703  
                                 
 704  0
                                 if (t.getTo() == null){
 705  0
                                         Source s = null;
 706  0
                                         if (getBudgetCategories().size() > 0)
 707  0
                                                 s = getBudgetCategories().get(0);
 708  0
                                         else if (getAccounts().size() > 0)
 709  0
                                                 s = getAccounts().get(0);
 710  
                                         else
 711  0
                                                 s = null;
 712  0
                                         String message = "Transaction with description '" + t.getDescription() + "' of amount '"+ t.getAmount() + "' on date '" + t.getDate() + "' does not have a To source defined.  Setting this to '" + s.getFullName();
 713  0
                                         logger.warning(message);
 714  0
                                         sb.append(message).append("\n\n");
 715  0
                                         t.setFrom(s);
 716  
                                 }
 717  
                         }
 718  0
                         catch (InvalidValueException ive){
 719  0
                                 String message = "Error while correcting corrupted transaction: " + ive.getMessage();
 720  0
                                 logger.severe(message);
 721  0
                                 sb.append(message).append("\n\n");
 722  0
                         }
 723  
                 }
 724  
                 
 725  3034
                 if (sb.length() > 0)
 726  0
                         return sb.toString();
 727  3034
                 return null;
 728  
         }
 729  
         
 730  
         /**
 731  
          * Refreshes the UID map.  This is a relatively expensive operation, and as such is 
 732  
          * generally only done at file load time.
 733  
          * 
 734  
          * We iterate through all objects for each data type, and add them to the UID map.
 735  
          * This will associate the object with their UID as the key in the Map.
 736  
          */
 737  
         public void refreshUidMap() throws ModelException {
 738  3034
                 uidMap.clear();
 739  
 
 740  3034
                 for (Account a : getAccounts()) {
 741  0
                         checkValid(a, false, true);
 742  0
                         registerObjectInUidMap(a);
 743  
                 }
 744  
 
 745  3034
                 for (BudgetCategory bc : getBudgetCategories()) {
 746  30340
                         checkValid(bc, false, true);
 747  30340
                         registerObjectInUidMap(bc);
 748  
                 }
 749  
 
 750  
 //                for (BudgetPeriodBean bpb : dataModel.getBudgetPeriods().values()) {
 751  
 //                BudgetPeriod bp = new BudgetPeriod(this, bpb);
 752  
 //                checkValid(bp, false, true);
 753  
 //                registerObjectInUidMap(bp);                        
 754  
 //                }
 755  
 
 756  3034
                 for (AccountType t : getAccountTypes()) {
 757  27306
                         checkValid(t, false, true);
 758  27306
                         registerObjectInUidMap(t);
 759  
                 }
 760  
 
 761  3034
                 for (Transaction t : getTransactions()) {
 762  0
                         checkValid(t, false, true);
 763  0
                         registerObjectInUidMap(t);        
 764  
                 }
 765  
 
 766  3034
                 for (ScheduledTransaction s : getScheduledTransactions()) {
 767  0
                         checkValid(s, false, true);
 768  0
                         registerObjectInUidMap(s);        
 769  
                 }
 770  3034
         }
 771  
 
 772  
         private void registerObjectInUidMap(ModelObject object){
 773  57646
                 uidMap.put(object.getUid(), object);
 774  57646
         }
 775  
 
 776  
         @Override
 777  
         public String toString() {
 778  0
                 StringBuilder sb = new StringBuilder();
 779  
 
 780  0
                 sb.append("\n--Accounts and Types--\n");
 781  0
                 for (AccountType t : getAccountTypes()) {
 782  0
                         sb.append(t).append("\n");
 783  
 
 784  0
                         for (Account a : new FilteredLists.AccountListFilteredByType(this, this.getAccounts(), t)) {
 785  0
                                 sb.append("\t").append(a).append("\n");
 786  
                         }
 787  
                 }
 788  
 
 789  0
                 sb.append("\n--Budget Categories--\n");
 790  
 
 791  
                 //We only show 3 levels here; if there are more, we figure that it is not needed to
 792  
                 // output them in the toString method, as it is just for troubleshooting.
 793  0
                 for (BudgetCategory bc : new FilteredLists.BudgetCategoryListFilteredByParent(this, this.getBudgetCategories(), null)) {
 794  0
                         sb.append(bc).append("\n");
 795  0
                         for (BudgetCategory child1 : new FilteredLists.BudgetCategoryListFilteredByParent(this, this.getBudgetCategories(), bc)) {
 796  0
                                 sb.append("\t").append(child1).append("\n");
 797  0
                                 for (BudgetCategory child2 : new FilteredLists.BudgetCategoryListFilteredByParent(this, this.getBudgetCategories(), child1)) {
 798  0
                                         sb.append("\t\t").append(child2).append("\n");        
 799  
                                 }                                
 800  
                         }
 801  
                 }
 802  
 
 803  
 //                sb.append("\n--Budget Periods--\n");
 804  
 //                List<String> periodDates = new LinkedList<String>(dataModel.getBudgetPeriods().keySet());
 805  
 //                Collections.sort(periodDates);
 806  
 //                for (String d : periodDates) {
 807  
 //                sb.append(d).append(getBudgetPeriod(d));
 808  
 //                }
 809  0
                 sb.append("\n--Transactions--\n");
 810  0
                 sb.append("Total transactions: ").append(getTransactions().size()).append("\n");
 811  0
                 for (Transaction t : getTransactions()) {
 812  0
                         sb.append(t.toString()).append("\n");
 813  
                 }
 814  
                 
 815  0
                 sb.append("\n--Scheduled Transactions--\n");
 816  0
                 for (ScheduledTransaction st : getScheduledTransactions()) {
 817  0
                         sb.append(st.toString()).append("\n");
 818  
                 }
 819  0
                 sb.append("--");
 820  
 
 821  0
                 return sb.toString();
 822  
         }
 823  
         
 824  
         public Time getModified() {
 825  0
                 return modifiedTime;
 826  
         }
 827  
 
 828  
         private void checkLists(){
 829  324836
                 if (this.accounts == null)
 830  0
                         this.accounts = new LinkedList<Account>();
 831  324836
                 if (this.transactions == null)
 832  0
                         this.transactions = new LinkedList<Transaction>();
 833  324836
                 if (this.scheduledTransactions == null)
 834  0
                         this.scheduledTransactions = new LinkedList<ScheduledTransaction>();
 835  324836
                 if (this.accountTypes == null)
 836  0
                         this.accountTypes = new LinkedList<AccountType>();
 837  324836
         }
 838  
 
 839  
         /**
 840  
          * Perform all the sanity checks here, for code simplicity
 841  
          * @param object The object to be modified
 842  
          * @param isAddOperation Is this an add operation? (This affects some of the checks,
 843  
          * such as if the UID is already entered into the model).
 844  
          * @param isUidRefresh Is this being called from the refreshUid method?
 845  
          */
 846  
         private void checkValid(ModelObject object, boolean isAddOperation, boolean isUidRefresh) throws ModelException {
 847  115292
                 if (object.getDocument() == null)
 848  0
                         throw new ModelException("Document has not yet been set for this object");
 849  
 
 850  115292
                 if(!object.getDocument().equals(this))
 851  0
                         throw new ModelException("Cannot modify an object not in this model");
 852  
 
 853  115292
                 if (isAddOperation){
 854  
 //                        if (this.getUid(object.getBean()) != null)
 855  
 //                        throw new DataModelProblemException("Cannot have the same UID for multiple objects in the model.", this);
 856  
 
 857  
 //                        if (object instanceof Account){
 858  
 //                                for (Account a : getAccounts()) {
 859  
 //                                        if (a.getName().equalsIgnoreCase(((Account) object).getName()))
 860  
 //                                                throw new ModelException("Cannot have multiple accounts with the same name");
 861  
 //                                }
 862  
 //                        }
 863  
                         
 864  57646
                         if (object instanceof AccountType){
 865  27306
                                 for (AccountType at : getAccountTypes()) {
 866  109224
                                         if (at.getName().equalsIgnoreCase(((AccountType) object).getName()))
 867  0
                                                 throw new ModelException("Cannot have multiple accounts types with the same name");
 868  
                                 }
 869  
                         }
 870  
 
 871  
 
 872  57646
                         if (object instanceof Transaction){
 873  0
                                 Transaction t = (Transaction) object;
 874  
                                 
 875  0
                                 if (t.getFrom() instanceof Split && t.getFromSplits() != null){
 876  0
                                         long splitSum = 0;
 877  0
                                         for (TransactionSplit split : t.getFromSplits()) {
 878  0
                                                 splitSum += split.getAmount();
 879  0
                                                 if (split.getSource() == null)
 880  0
                                                         throw new ModelException("Cannot have a null source within a TransactionSplit object.");
 881  0
                                                 if (split.getAmount() == 0)
 882  0
                                                         throw new ModelException("Cannot have an amount equal to zero within a TransactionSplit object.");
 883  0
                                                 if (split.getSource() instanceof BudgetCategory && !((BudgetCategory) split.getSource()).isIncome())
 884  0
                                                         throw new ModelException("All Budget Category splits in the From position must be income categories.");
 885  
                                         }
 886  
                                         
 887  0
                                         if (splitSum != t.getAmount())
 888  0
                                                 throw new ModelException("The sum of the From splits do not equal the transaction amount");
 889  0
                                 }
 890  
                                 else {
 891  0
                                         if (t.getFrom() instanceof BudgetCategory && !((BudgetCategory) t.getFrom()).isIncome()){
 892  0
                                                 throw new ModelException("Budget Categories in the From position must be income categories.");
 893  
                                         }
 894  
                                 }
 895  
                                 
 896  0
                                 if (t.getTo() instanceof Split && t.getToSplits() != null){
 897  0
                                         long splitSum = 0;
 898  0
                                         for (TransactionSplit split : t.getToSplits()) {
 899  0
                                                 splitSum += split.getAmount();
 900  0
                                                 if (split.getSource() == null)
 901  0
                                                         throw new ModelException("Cannot have a null source within a TransactionSplit object.");
 902  0
                                                 if (split.getAmount() == 0)
 903  0
                                                         throw new ModelException("Cannot have an amount equal to zero within a TransactionSplit object.");
 904  0
                                                 if (split.getSource() instanceof BudgetCategory && ((BudgetCategory) split.getSource()).isIncome())
 905  0
                                                         throw new ModelException("All Budget Category splits in the To position must be expense categories.");
 906  
                                         }
 907  
                                         
 908  0
                                         if (splitSum != t.getAmount())
 909  0
                                                 throw new ModelException("The sum of the To splits do not equal the transaction amount");
 910  0
                                 }
 911  
                                 else {
 912  0
                                         if (t.getTo() instanceof BudgetCategory && ((BudgetCategory) t.getTo()).isIncome()){
 913  0
                                                 throw new ModelException("Budget Categories in the To position must be expense categories.");
 914  
                                         }
 915  
                                 }
 916  
                                 
 917  
                         }
 918  
                         
 919  
                         //We currently don't check for duplicate names.  Some instances (such as Perfitrack plugin) may require duplicates; also, 
 920  
                         // other than the potential to be confused with two identical names, there is no problem having duplicates. 
 921  
 //                        if (object instanceof BudgetCategory){
 922  
 //                                for (BudgetCategory bc : getBudgetCategories()) {
 923  
 //                                        //If the two budget categories are both children of the same node (which can be null), 
 924  
 //                                        // and the name is the same, throw an exception.
 925  
 //                                        if (bc.getName().equalsIgnoreCase(((BudgetCategory) object).getName())
 926  
 //                                                        && ((bc.getParent() == null && ((BudgetCategory) object).getParent() == null)
 927  
 //                                                                        || (bc.getParent() != null && ((BudgetCategory) object).getParent() != null && bc.getParent().equals(((BudgetCategory) object).getParent()))))
 928  
 //                                                throw new ModelException("Cannot have multiple budget categories with the same name as children of the same node");
 929  
 //                                }
 930  
 //                        }
 931  
 
 932  57646
                         if (object instanceof ScheduledTransaction){
 933  0
                                 for (ScheduledTransaction s : getScheduledTransactions()) {
 934  0
                                         if (s.getScheduleName().equalsIgnoreCase(((ScheduledTransaction) object).getScheduleName()))
 935  0
                                                 throw new ModelException("Cannot have multiple scheduled transactions with the same name");
 936  
                                 }                                
 937  
                         }
 938  
                 }
 939  
 
 940  115292
                 if (isAddOperation || isUidRefresh){
 941  115292
                         if (uidMap.get(object.getUid()) != null)
 942  0
                                 throw new DataModelProblemException("Identical UID already in model!  Model is probably corrupt!", this); 
 943  
                 }
 944  
 
 945  115292
         }
 946  
         
 947  
         /**
 948  
          * Runs through the list of scheduled transactions, and adds any which
 949  
          * show be executed to the apropriate transacactions list.
 950  
 +          * Checks for the frequency type and based on it finds if a transaction is scheduled for a date
 951  
 +          * that has gone past.
 952  
          */
 953  
         public void updateScheduledTransactions(){
 954  0
                 updateScheduledTransactions(new Date());
 955  0
         }
 956  
 
 957  
         /**
 958  
          * Runs through the list of scheduled transactions, and adds any which
 959  
          * show be executed to the appropriate transactions list.
 960  
           * Checks for the frequency type and based on it finds if a transaction is scheduled for a date
 961  
           * that has gone past.
 962  
           * 
 963  
           * This method includes an argument to specify what the current date is.  This can
 964  
           * be useful if you want to add transactions after the current date.
 965  
          */
 966  
         public void updateScheduledTransactions(Date currentDate){
 967  0
                 startBatchChange();
 968  
 
 969  
                 //Update any scheduled transactions
 970  0
                 final Date today = DateUtil.getEndOfDay(currentDate);
 971  
                 //We specify a GregorianCalendar because we make some assumptions
 972  
                 // about numbering of months, etc that may break if we 
 973  
                 // use the default calendar for the locale.  It's not the
 974  
                 // prettiest code, but it works.  Perhaps we can change
 975  
                 // it to be cleaner later on...
 976  0
                 final GregorianCalendar tempCal = new GregorianCalendar();
 977  
 
 978  0
                 for (ScheduledTransaction s : new FilteredLists.ScheduledTransactionListFilteredByBeforeToday(this, getScheduledTransactions())) {
 979  0
                         if (Const.DEVEL) logger.info("Looking at scheduled transaction " + s.getScheduleName());
 980  
 
 981  0
                         Date tempDate = s.getLastDayCreated();
 982  
                         //#1779286 Bug BiWeekly Scheduled Transactions -Check if this transaction has never been created. 
 983  0
                         boolean isNewTransaction=false;
 984  
                         //The lastDayCreated need to date as such without rolling forward by a day and the start fo the day 
 985  
                         //so calculations of difference of days are on the same keel as tempDate.
 986  0
                         Date lastDayCreated = null;
 987  
                         //Temp date is where we will start looping from.
 988  0
                         if (tempDate == null){
 989  
                                 //If it is null, we need to init it to a sane value.
 990  0
                                 tempDate = s.getStartDate();
 991  0
                                 isNewTransaction=true;
 992  
                                 //The below is just to avoid NPE's; ideally changing the order 
 993  
                                 // of the checking below will solve the problem, but better safe than sorry.
 994  
                                 //The reason we set this date to an impossibly early date is to ensure
 995  
                                 // that we include a scheduled transaction on the first day that matches,
 996  
                                 // even if that day is the first day of any scheduled transactions.
 997  0
                                 lastDayCreated=DateUtil.getDate(1900);
 998  
                         }
 999  
                         else {
 1000  0
                                 lastDayCreated = DateUtil.getStartOfDay(tempDate);
 1001  
                                 //We start one day after the last day, to avoid repeats.  
 1002  
                                 // See bug #1641937 for more details.
 1003  0
                                 tempDate = DateUtil.addDays(tempDate, 1);
 1004  
 
 1005  
                         }
 1006  
 
 1007  
 
 1008  
 
 1009  0
                         tempDate = DateUtil.getStartOfDay(tempDate);
 1010  
 
 1011  0
                         if (Const.DEVEL){
 1012  0
                                 logger.finest("tempDate = " + tempDate);
 1013  0
                                 logger.finest("startDate = " + s.getStartDate());
 1014  0
                                 logger.finest("endDate = " + s.getEndDate());
 1015  
                         }
 1016  
 
 1017  
                         //The transaction is scheduled for a date before today and before the EndDate 
 1018  
                         while (tempDate.before(today) 
 1019  0
                                         && (s.getEndDate() == null 
 1020  
                                                         || s.getEndDate().after(tempDate)
 1021  
                                                         || (DateUtil.getDaysBetween(s.getEndDate(), tempDate, false) == 0))) {
 1022  0
                                 if (Const.DEVEL) logger.finest("Trying date " + tempDate);
 1023  
 
 1024  
                                 //We use a Calendar instead of a Date object for comparisons
 1025  
                                 // because the Calendar interface is much nicer.
 1026  0
                                 tempCal.setTime(tempDate);
 1027  
 
 1028  0
                                 boolean todayIsTheDay = false;
 1029  
 
 1030  
                                 //We check each type of schedule, and if it matches,
 1031  
                                 // we set todayIsTheDay to true.  We could do it 
 1032  
                                 // all in one huge if statement, but that is very
 1033  
                                 // hard to read and maintain.
 1034  
 
 1035  
                                 //If we are using the Monthly by Date frequency, 
 1036  
                                 // we only check if the given day is equal to the
 1037  
                                 // scheduled day.
 1038  0
                                 if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_MONTHLY_BY_DATE.toString())
 1039  
                                                 && (s.getScheduleDay() == tempCal.get(Calendar.DAY_OF_MONTH) 
 1040  
                                                                 || (s.getScheduleDay() == 32 //Position 32 is 'Last Day of Month'.  ScheduleFrequencyDayOfMonth.SCHEDULE_DATE_LAST_DAY.ordinal() + 1
 1041  
                                                                                 && tempCal.get(Calendar.DAY_OF_MONTH) == tempCal.getActualMaximum(Calendar.DAY_OF_MONTH)))){
 1042  
 
 1043  0
                                         todayIsTheDay = true;
 1044  
                                 }
 1045  
                                 //If we are using the Monthly by Day of Week,
 1046  
                                 // we check if the given day (Sunday, Monday, etc) is equal to the
 1047  
                                 // scheduleDay, and if the given day is within the first week.
 1048  
                                 // FYI, we store Sunday == 0, even though Calendar.SUNDAY == 1.  Thus,
 1049  
                                 // we add 1 to our stored day before comparing it.
 1050  0
                                 else if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_MONTHLY_BY_DAY_OF_WEEK.toString())
 1051  
                                                 && s.getScheduleDay() + 1 == tempCal.get(Calendar.DAY_OF_WEEK)
 1052  
                                                 && tempCal.get(Calendar.DAY_OF_MONTH) <= 7){
 1053  0
                                         todayIsTheDay = true;
 1054  
                                 }
 1055  
                                 //If we are using Weekly frequency, we only need to compare
 1056  
                                 // the number of the day.
 1057  
                                 // FYI, we store Sunday == 0, even though Calendar.SUNDAY == 1.  Thus,
 1058  
                                 // we add 1 to our stored day before comparing it.
 1059  0
                                 else if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_WEEKLY.toString())
 1060  
                                                 && s.getScheduleDay() + 1 == tempCal.get(Calendar.DAY_OF_WEEK)){
 1061  0
                                         todayIsTheDay = true;
 1062  
                                 }
 1063  
                                 //If we are using BiWeekly frequency, we need to compare
 1064  
                                 // the number of the day as well as ensure that there is one
 1065  
                                 // week between each scheduled transaction.
 1066  
                                 // FYI, we store Sunday == 0, even though Calendar.SUNDAY == 1.  Thus,
 1067  
                                 // we add 1 to our stored day before comparing it.
 1068  0
                                 else if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_BIWEEKLY.toString())
 1069  
                                                 && s.getScheduleDay() + 1 == tempCal.get(Calendar.DAY_OF_WEEK)
 1070  
                                                 //As tempdate has been moved forward by one day we need to check if it is >= 13 instead of >13
 1071  
                                                 && ((DateUtil.getDaysBetween(lastDayCreated, tempDate, false) >= 13)
 1072  
                                                                 || isNewTransaction)){
 1073  0
                                         todayIsTheDay = true;
 1074  0
                                         lastDayCreated = (Date) tempDate.clone();
 1075  0
                                         if(isNewTransaction){
 1076  0
                                                 isNewTransaction=false;
 1077  
                                         }
 1078  
                                 }
 1079  
                                 //Every X days, where X is the value in s.getScheduleDay().  Check if we
 1080  
                                 // have passed the correct number of days since the last transaction.
 1081  0
                                 else if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_EVERY_X_DAYS.toString())
 1082  
                                                 && DateUtil.getDaysBetween(lastDayCreated, tempDate, false) >= s.getScheduleDay() ){
 1083  0
                                         todayIsTheDay = true;
 1084  0
                                         lastDayCreated = (Date) tempDate.clone();
 1085  
                                 }
 1086  
                                 //Every day - it's obvious enough even for a monkey!
 1087  0
                                 else if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_EVERY_DAY.toString())){
 1088  0
                                         todayIsTheDay = true;
 1089  
                                 }
 1090  
                                 //Every weekday - all days but Saturday and Sunday.
 1091  0
                                 else if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_EVERY_WEEKDAY.toString())
 1092  
                                                 && (tempCal.get(Calendar.DAY_OF_WEEK) < Calendar.SATURDAY)
 1093  
                                                 && (tempCal.get(Calendar.DAY_OF_WEEK) > Calendar.SUNDAY)){
 1094  0
                                         todayIsTheDay = true;
 1095  
                                 }
 1096  
                                 //To make this one clearer, we do it in two passes.
 1097  
                                 // First, we check the frequency type and the day.
 1098  
                                 // If these match, we do our bit bashing to determine
 1099  
                                 // if the week is correct.
 1100  0
                                 else if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_MULTIPLE_WEEKS_EVERY_MONTH.toString())
 1101  
                                                 && s.getScheduleDay() + 1 == tempCal.get(Calendar.DAY_OF_WEEK)){
 1102  0
                                         if (Const.DEVEL) {
 1103  0
                                                 logger.finest("We are looking at day " + tempCal.get(Calendar.DAY_OF_WEEK) + ", which matches s.getScheduleDay() which == " + s.getScheduleDay());
 1104  0
                                                 logger.finest("s.getScheduleWeek() == " + s.getScheduleWeek());
 1105  
                                         }
 1106  0
                                         int week = s.getScheduleWeek();
 1107  
                                         //The week mask should return 1 for the first week (day 1 - 7), 
 1108  
                                         // 2 for the second week (day 8 - 14), 4 for the third week (day 15 - 21),
 1109  
                                         // and 8 for the fourth week (day 22 - 28).  We then AND it with 
 1110  
                                         // the scheduleWeek to determine if this week matches the criteria
 1111  
                                         // or not.
 1112  0
                                         int weekNumber = tempCal.get(Calendar.DAY_OF_WEEK_IN_MONTH) - 1;
 1113  0
                                         int weekMask = (int) Math.pow(2, weekNumber);
 1114  0
                                         if (Const.DEVEL){
 1115  0
                                                 logger.finest("The week number is " + weekNumber + ", the week mask is " + weekMask + ", and the day of week in month is " + tempCal.get(Calendar.DAY_OF_WEEK_IN_MONTH));
 1116  
                                         }
 1117  0
                                         if ((week & weekMask) != 0){
 1118  0
                                                 if (Const.DEVEL) logger.info("The date " + tempCal.getTime() + " matches the requirements.");
 1119  0
                                                 todayIsTheDay = true;
 1120  
                                         }
 1121  0
                                 }
 1122  
                                 //To make this one clearer, we do it in two passes.
 1123  
                                 // First, we check the frequency type and the day.
 1124  
                                 // If these match, we do our bit bashing to determine
 1125  
                                 // if the month is correct.
 1126  0
                                 else if (s.getFrequencyType().equals(ScheduleFrequency.SCHEDULE_FREQUENCY_MULTIPLE_MONTHS_EVERY_YEAR.toString())
 1127  
                                                 && s.getScheduleDay() == tempCal.get(Calendar.DAY_OF_MONTH)){
 1128  0
                                         int months = s.getScheduleMonth();
 1129  
                                         //The month mask should be 2 ^ MONTH NUMBER,
 1130  
                                         // where January == 0.
 1131  
                                         // i.e. 1 for January, 4 for March, 2048 for December.
 1132  0
                                         int monthMask = (int) Math.pow(2, tempCal.get(Calendar.MONTH));
 1133  0
                                         if ((months & monthMask) != 0){
 1134  0
                                                 if (Const.DEVEL) logger.info("The date " + tempCal.getTime() + " matches the requirements.");
 1135  0
                                                 todayIsTheDay = true;
 1136  
                                         }
 1137  
                                 }
 1138  
 
 1139  
                                 //Check that there has not already been a scheduled transaction with identical
 1140  
                                 // paramters for this day.  This is in response to a potential bug where
 1141  
                                 // the last scheduled day is missing (happened once in development 
 1142  
                                 // version, but may not be a repeating problem).
 1143  
                                 //This has the potential to skip scheduled transactions, if there
 1144  
                                 // are multiple scheduled transactions which go to and from the 
 1145  
                                 // same accounts / categories on the same day.  If this proves to
 1146  
                                 // be a problem, we may make the checks more specific.
 1147  0
                                 if (todayIsTheDay){
 1148  0
                                         for (Transaction t : getTransactions(tempDate, tempDate)) {
 1149  0
                                                 if (DateUtil.isSameDay(t.getDate(), tempDate)
 1150  
                                                                 && t.isScheduled()
 1151  
                                                                 && t.getFrom().equals(s.getFrom())
 1152  
                                                                 && t.getTo().equals(s.getTo())
 1153  
                                                                 && t.getDescription().equals(s.getDescription())){
 1154  0
                                                         todayIsTheDay = false;
 1155  
                                                         try {
 1156  0
                                                                 s.setLastDayCreated(tempDate);
 1157  
                                                         }
 1158  0
                                                         catch (InvalidValueException ive){
 1159  0
                                                                 logger.warning("Error setting last created date");
 1160  0
                                                         }
 1161  
                                                 }
 1162  
                                         }
 1163  
                                 }
 1164  
                                 
 1165  
                                 
 1166  
                                 try {
 1167  
                                         //If one of the above rules matches, we will copy the
 1168  
                                         // scheduled transaction into the transactions list
 1169  
                                         // at the given day.
 1170  0
                                         if (todayIsTheDay){
 1171  
                                                 
 1172  0
                                                 if (Const.DEVEL) logger.finest("Setting last created date to " + tempDate);
 1173  0
                                                 s.setLastDayCreated(DateUtil.getEndOfDay(tempDate));
 1174  0
                                                 if (Const.DEVEL) logger.finest("Last created date to " + s.getLastDayCreated());
 1175  
 
 1176  0
                                                 if (s.getMessage() != null && s.getMessage().trim().length() > 0){
 1177  0
                                                         String[] options = new String[1];
 1178  0
                                                         options[0] = TextFormatter.getTranslation(ButtonKeys.BUTTON_OK);
 1179  
 
 1180  0
                                                         JOptionPane.showOptionDialog(
 1181  
                                                                         null, 
 1182  
                                                                         s.getMessage(), 
 1183  
                                                                         TextFormatter.getTranslation(BuddiKeys.SCHEDULED_MESSAGE), 
 1184  
                                                                         JOptionPane.DEFAULT_OPTION,
 1185  
                                                                         JOptionPane.INFORMATION_MESSAGE,
 1186  
                                                                         null,
 1187  
                                                                         options,
 1188  
                                                                         options[0]
 1189  
                                                         );
 1190  
                                                 }
 1191  
 
 1192  0
                                                 if (tempDate != null
 1193  
                                                                 && s.getDescription() != null) {
 1194  0
                                                         Transaction t = ModelFactory.createTransaction(
 1195  
                                                                         tempDate, 
 1196  
                                                                         s.getDescription(), 
 1197  
                                                                         s.getAmount(), 
 1198  
                                                                         s.getFrom(), 
 1199  
                                                                         s.getTo());
 1200  
 
 1201  0
                                                         t.setDate(tempDate);
 1202  0
                                                         t.setDescription(s.getDescription());
 1203  0
                                                         t.setAmount(s.getAmount());
 1204  0
                                                         t.setTo(s.getTo());
 1205  0
                                                         t.setFrom(s.getFrom());
 1206  0
                                                         t.setMemo(s.getMemo());
 1207  0
                                                         t.setNumber(s.getNumber());
 1208  
 //                                                        t.setCleared(s.isCleared());
 1209  
 //                                                        t.setReconciled(s.isReconciled());
 1210  0
                                                         t.setScheduled(true);
 1211  
 
 1212  0
                                                         this.addTransaction(t);
 1213  0
                                                         if (Const.DEVEL) logger.info("Added scheduled transaction " + t + " to transaction list on date " + t.getDate());
 1214  0
                                                 }
 1215  
                                         }
 1216  
                                         else {
 1217  0
                                                 logger.finest("Today was not the day to add the scheduled transaction...\n\tDate = " 
 1218  
                                                                 + tempDate
 1219  
                                                                 + "\n\tDescription = " 
 1220  
                                                                 + s.getDescription());
 1221  
                                         }
 1222  
 
 1223  
                                 }
 1224  0
                                 catch (ModelException me){
 1225  0
                                         logger.log(Level.WARNING, "Error adding scheduled tranaction", me);
 1226  0
                                 }
 1227  
 
 1228  0
                                 tempDate = DateUtil.addDays(tempDate, 1);
 1229  0
                         }
 1230  0
                 }
 1231  
 
 1232  0
                 finishBatchChange();
 1233  0
                 updateAllBalances();
 1234  0
         }
 1235  
 
 1236  
         public void setPassword(char[] password) {
 1237  0
                 this.password = password;
 1238  0
         }
 1239  
         
 1240  
         public long getNetWorth(Date date) {
 1241  15154
                 List<Account> accounts = getAccounts();
 1242  15154
                 long total = 0; 
 1243  
                 
 1244  15154
                 for (Account a : accounts) {                        
 1245  0
                         if (!a.isDeleted()){
 1246  0
                                 if (date == null)
 1247  0
                                         total += a.getBalance();
 1248  
                                 else
 1249  0
                                         total += a.getBalance(date);
 1250  
                         }
 1251  
                 }
 1252  
 
 1253  15154
                 return total;
 1254  
         }
 1255  
         
 1256  
 
 1257  
         /**
 1258  
          * Performs a deep clone of the Document model.  This will result in a completely
 1259  
          * different object, with all component objects different, but with all the same
 1260  
          * values.  Immutable objects (such as Strings) and primitives may be identical 
 1261  
          * between cloned and source objects, but all immutable objects will not be identical.  
 1262  
          * @param source
 1263  
          * @return
 1264  
          */
 1265  
         public Document clone() throws CloneNotSupportedException {
 1266  0
                 updateAllBalances();        //We want to be sure that out original object is in the correct state before cloning.
 1267  
                 
 1268  
                 try {
 1269  0
                         Map<ModelObject, ModelObject> originalToClonedObjectMap = new HashMap<ModelObject, ModelObject>();
 1270  
 
 1271  0
                         DocumentImpl clone = new DocumentImpl();
 1272  0
                         clone.startBatchChange();
 1273  0
                         clone.setFile(this.getFile());
 1274  0
                         clone.flags = flags;
 1275  0
                         clone.modifiedTime = new Time(modifiedTime);
 1276  0
                         if (password != null)
 1277  0
                                 clone.password = new String(password).toCharArray();
 1278  
 
 1279  0
                         originalToClonedObjectMap.put(this, clone);
 1280  
                         
 1281  
                         //Clone all component objects.
 1282  0
                         for (AccountType old : this.getAccountTypes()) {
 1283  0
                                 clone.addAccountType((AccountType) ((AccountTypeImpl) old).clone(originalToClonedObjectMap));
 1284  
                         }
 1285  0
                         for (Account old : this.getAccounts()) {
 1286  0
                                 clone.addAccount((Account) ((AccountImpl) old).clone(originalToClonedObjectMap));
 1287  
                         }
 1288  0
                         for (BudgetCategory old : this.getBudgetCategories()) {
 1289  0
                                 clone.addBudgetCategory((BudgetCategory) ((BudgetCategoryImpl) old).clone(originalToClonedObjectMap));
 1290  
                         }
 1291  0
                         for (ScheduledTransaction old : this.getScheduledTransactions()) {
 1292  0
                                 clone.addScheduledTransaction((ScheduledTransaction) ((ScheduledTransactionImpl) old).clone(originalToClonedObjectMap));
 1293  
                         }
 1294  0
                         for (Transaction old : this.getTransactions()) {
 1295  0
                                 clone.addTransaction((Transaction) ((TransactionImpl) old).clone(originalToClonedObjectMap));
 1296  
                         }
 1297  
 
 1298  0
                         clone.refreshUidMap();
 1299  0
                         clone.finishBatchChange();
 1300  
 
 1301  0
                         return clone;
 1302  
                 }
 1303  0
                 catch (ModelException me){
 1304  0
                         throw new CloneNotSupportedException(me.getMessage());
 1305  
                 }
 1306  
         }
 1307  
 }