Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
DocumentImpl |
|
| 4.202898550724638;4.203 | ||||
DocumentImpl$1 |
|
| 4.202898550724638;4.203 | ||||
DocumentImpl$2 |
|
| 4.202898550724638;4.203 | ||||
DocumentImpl$3 |
|
| 4.202898550724638;4.203 | ||||
DocumentImpl$4 |
|
| 4.202898550724638;4.203 | ||||
DocumentImpl$5 |
|
| 4.202898550724638;4.203 | ||||
DocumentImpl$6 |
|
| 4.202898550724638;4.203 | ||||
DocumentImpl$7 |
|
| 4.202898550724638;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 | } |