X-Git-Url: https://git.ktnx.net/?p=mobile-ledger-staging.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fnet%2Fktnx%2Fmobileledger%2Fasync%2FRetrieveTransactionsTask.java;h=1d69fa2941a1f0963f5857c560f15bf244e2d751;hp=ef6eb8172c0598f3088af2c5481be4073042862f;hb=20c03b7a5eb152d42fbbe9ecbaae27530563b398;hpb=5e5e48984b4239b548a9d95a99323b874928e32f diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java index ef6eb817..1d69fa29 100644 --- a/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java @@ -36,31 +36,30 @@ import net.ktnx.mobileledger.model.LedgerAccount; import net.ktnx.mobileledger.model.LedgerTransaction; import net.ktnx.mobileledger.model.LedgerTransactionAccount; import net.ktnx.mobileledger.model.MobileLedgerProfile; -import net.ktnx.mobileledger.ui.activity.MainActivity; +import net.ktnx.mobileledger.ui.MainModel; import net.ktnx.mobileledger.utils.NetworkUtil; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.lang.ref.WeakReference; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; -import java.util.Stack; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static net.ktnx.mobileledger.utils.Logger.debug; - -public class RetrieveTransactionsTask - extends AsyncTask { +public class RetrieveTransactionsTask extends + AsyncTask { private static final int MATCHING_TRANSACTIONS_LIMIT = 150; private static final Pattern reComment = Pattern.compile("^\\s*;"); private static final Pattern reTransactionStart = Pattern.compile( @@ -74,16 +73,21 @@ public class RetrieveTransactionsTask private static final Pattern reEnd = Pattern.compile("\\bid=\"addmodal\""); private static final Pattern reDecimalPoint = Pattern.compile("\\.\\d\\d?$"); private static final Pattern reDecimalComma = Pattern.compile(",\\d\\d?$"); - private WeakReference contextRef; // %3A is '=' - private Pattern reAccountName = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\""); - private Pattern reAccountValue = Pattern.compile( + private final Pattern reAccountName = + Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\""); + private final Pattern reAccountValue = Pattern.compile( "\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?"); - private MobileLedgerProfile profile; - public RetrieveTransactionsTask(WeakReference contextRef, - @NonNull MobileLedgerProfile profile) { - this.contextRef = contextRef; + private final MainModel mainModel; + private final MobileLedgerProfile profile; + private final List prevAccounts; + private int expectedPostingsCount = -1; + public RetrieveTransactionsTask(@NonNull MainModel mainModel, + @NonNull MobileLedgerProfile profile, + List accounts) { + this.mainModel = mainModel; this.profile = profile; + this.prevAccounts = accounts; } private static void L(String msg) { //debug("transaction-parser", msg); @@ -94,7 +98,7 @@ public class RetrieveTransactionsTask String postingStatus = m.group(1); String acc_name = m.group(2); String currencyPre = m.group(3); - String amount = m.group(4); + String amount = Objects.requireNonNull(m.group(4)); String currencyPost = m.group(5); String currency = null; @@ -109,7 +113,7 @@ public class RetrieveTransactionsTask amount = amount.replace(',', '.'); - return new LedgerTransactionAccount(acc_name, Float.valueOf(amount), currency, null); + return new LedgerTransactionAccount(acc_name, Float.parseFloat(amount), currency, null); } else { return null; @@ -118,43 +122,33 @@ public class RetrieveTransactionsTask @Override protected void onProgressUpdate(Progress... values) { super.onProgressUpdate(values); - MainActivity context = getContext(); - if (context == null) - return; - context.onRetrieveProgress(values[0]); - } - @Override - protected void onPreExecute() { - super.onPreExecute(); - MainActivity context = getContext(); - if (context == null) - return; - context.onRetrieveStart(); + Data.backgroundTaskProgress.postValue(values[0]); } @Override - protected void onPostExecute(String error) { - super.onPostExecute(error); - MainActivity context = getContext(); - if (context == null) - return; - context.onRetrieveDone(error); + protected void onPostExecute(Result result) { + super.onPostExecute(result); + Progress progress = new Progress(); + progress.setState(ProgressState.FINISHED); + progress.setError(result.error); + onProgressUpdate(progress); } @Override protected void onCancelled() { super.onCancelled(); - MainActivity context = getContext(); - if (context == null) - return; - context.onRetrieveDone(null); - } - private String retrieveTransactionListLegacy() - throws IOException, ParseException, HTTPException { Progress progress = new Progress(); - int maxTransactionId = Progress.INDETERMINATE; - ArrayList accountList = new ArrayList<>(); - HashMap accountNames = new HashMap<>(); - HashMap syntheticAccounts = new HashMap<>(); - LedgerAccount lastAccount = null, prevAccount = null; + progress.setState(ProgressState.FINISHED); + onProgressUpdate(progress); + } + private void retrieveTransactionListLegacy(List accounts, + List transactions) + throws IOException, HTTPException { + Progress progress = Progress.indeterminate(); + progress.setState(ProgressState.RUNNING); + progress.setTotal(expectedPostingsCount); + int maxTransactionId = -1; + HashMap map = new HashMap<>(); + LedgerAccount lastAccount = null; + ArrayList syntheticAccounts = new ArrayList<>(); HttpURLConnection http = NetworkUtil.prepareConnection(profile, "journal"); http.setAllowUserInteraction(false); @@ -162,235 +156,174 @@ public class RetrieveTransactionsTask if (http.getResponseCode() != 200) throw new HTTPException(http.getResponseCode(), http.getResponseMessage()); - SQLiteDatabase db = App.getDatabase(); try (InputStream resp = http.getInputStream()) { if (http.getResponseCode() != 200) throw new IOException(String.format("HTTP error %d", http.getResponseCode())); - db.beginTransaction(); - try { - prepareDbForRetrieval(db, profile); - - int matchedTransactionsCount = 0; + int matchedTransactionsCount = 0; - ParserState state = ParserState.EXPECTING_ACCOUNT; - String line; - BufferedReader buf = - new BufferedReader(new InputStreamReader(resp, StandardCharsets.UTF_8)); + ParserState state = ParserState.EXPECTING_ACCOUNT; + String line; + BufferedReader buf = + new BufferedReader(new InputStreamReader(resp, StandardCharsets.UTF_8)); - int processedTransactionCount = 0; - int transactionId = 0; - LedgerTransaction transaction = null; - LINES: - while ((line = buf.readLine()) != null) { - throwIfCancelled(); - Matcher m; - m = reComment.matcher(line); - if (m.find()) { - // TODO: comments are ignored for now + int processedTransactionCount = 0; + int transactionId = 0; + LedgerTransaction transaction = null; + LINES: + while ((line = buf.readLine()) != null) { + throwIfCancelled(); + Matcher m; + m = reComment.matcher(line); + if (m.find()) { + // TODO: comments are ignored for now // Log.v("transaction-parser", "Ignoring comment"); - continue; - } - //L(String.format("State is %d", updating)); - switch (state) { - case EXPECTING_ACCOUNT: - if (line.equals("

General Journal

")) { - state = ParserState.EXPECTING_TRANSACTION; - L("→ expecting transaction"); - // commit the current transaction and start a new one - // the account list in the UI should reflect the (committed) - // state of the database - db.setTransactionSuccessful(); - db.endTransaction(); - profile.setAccounts(accountList); - db.beginTransaction(); + continue; + } + //L(String.format("State is %d", updating)); + switch (state) { + case EXPECTING_ACCOUNT: + if (line.equals("

General Journal

")) { + state = ParserState.EXPECTING_TRANSACTION; + L("→ expecting transaction"); + continue; + } + m = reAccountName.matcher(line); + if (m.find()) { + String acct_encoded = m.group(1); + String accName = URLDecoder.decode(acct_encoded, "UTF-8"); + accName = accName.replace("\"", ""); + L(String.format("found account: %s", accName)); + + lastAccount = map.get(accName); + if (lastAccount != null) { + L(String.format("ignoring duplicate account '%s'", accName)); continue; } - m = reAccountName.matcher(line); - if (m.find()) { - String acct_encoded = m.group(1); - String acct_name = URLDecoder.decode(acct_encoded, "UTF-8"); - acct_name = acct_name.replace("\"", ""); - L(String.format("found account: %s", acct_name)); - - prevAccount = lastAccount; - lastAccount = profile.tryLoadAccount(db, acct_name); - if (lastAccount == null) - lastAccount = new LedgerAccount(profile, acct_name); - else - lastAccount.removeAmounts(); - profile.storeAccount(db, lastAccount); - - if (prevAccount != null) - prevAccount.setHasSubAccounts( - prevAccount.isParentOf(lastAccount)); - // make sure the parent account(s) are present, - // synthesising them if necessary - // this happens when the (missing-in-HTML) parent account has - // only one child so we create a synthetic parent account record, - // copying the amounts when child's amounts are parsed - String parentName = lastAccount.getParentName(); - if (parentName != null) { - Stack toAppend = new Stack<>(); - while (parentName != null) { - if (accountNames.containsKey(parentName)) - break; - toAppend.push(parentName); - parentName = new LedgerAccount(profile, parentName).getParentName(); - } - syntheticAccounts.clear(); - while (!toAppend.isEmpty()) { - String aName = toAppend.pop(); - LedgerAccount acc = profile.tryLoadAccount(db, aName); - if (acc == null) { - acc = new LedgerAccount(profile, aName); - acc.setExpanded(!lastAccount.hasSubAccounts() || - lastAccount.isExpanded()); - } - acc.setHasSubAccounts(true); - acc.removeAmounts(); // filled below when amounts are - // parsed - if (acc.isVisible(accountList)) - accountList.add(acc); - L(String.format("gap-filling with %s", aName)); - accountNames.put(aName, null); - profile.storeAccount(db, acc); - syntheticAccounts.put(aName, acc); - } - } + String parentAccountName = LedgerAccount.extractParentName(accName); + LedgerAccount parentAccount; + if (parentAccountName != null) { + parentAccount = ensureAccountExists(parentAccountName, map, + syntheticAccounts); + } + else { + parentAccount = null; + } + lastAccount = new LedgerAccount(profile, accName, parentAccount); - if (lastAccount.isVisible(accountList)) - accountList.add(lastAccount); - accountNames.put(acct_name, null); + accounts.add(lastAccount); + map.put(accName, lastAccount); - state = ParserState.EXPECTING_ACCOUNT_AMOUNT; - L("→ expecting account amount"); - } - break; - - case EXPECTING_ACCOUNT_AMOUNT: - m = reAccountValue.matcher(line); - boolean match_found = false; - while (m.find()) { - throwIfCancelled(); - - match_found = true; - String value = m.group(1); - String currency = m.group(2); - if (currency == null) - currency = ""; - - { - Matcher tmpM = reDecimalComma.matcher(value); - if (tmpM.find()) { - value = value.replace(".", ""); - value = value.replace(',', '.'); - } - - tmpM = reDecimalPoint.matcher(value); - if (tmpM.find()) { - value = value.replace(",", ""); - value = value.replace(" ", ""); - } + state = ParserState.EXPECTING_ACCOUNT_AMOUNT; + L("→ expecting account amount"); + } + break; + + case EXPECTING_ACCOUNT_AMOUNT: + m = reAccountValue.matcher(line); + boolean match_found = false; + while (m.find()) { + throwIfCancelled(); + + match_found = true; + String value = Objects.requireNonNull(m.group(1)); + String currency = m.group(2); + if (currency == null) + currency = ""; + + { + Matcher tmpM = reDecimalComma.matcher(value); + if (tmpM.find()) { + value = value.replace(".", ""); + value = value.replace(',', '.'); } - L("curr=" + currency + ", value=" + value); - final float val = Float.parseFloat(value); - profile.storeAccountValue(db, lastAccount.getName(), currency, val); - lastAccount.addAmount(val, currency); - for (LedgerAccount syn : syntheticAccounts.values()) { - L(String.format(Locale.ENGLISH, "propagating %s %1.2f to %s", - currency, val, syn.getName())); - syn.addAmount(val, currency); - profile.storeAccountValue(db, syn.getName(), currency, val); + + tmpM = reDecimalPoint.matcher(value); + if (tmpM.find()) { + value = value.replace(",", ""); + value = value.replace(" ", ""); } } - - if (match_found) { - syntheticAccounts.clear(); - state = ParserState.EXPECTING_ACCOUNT; - L("→ expecting account"); + L("curr=" + currency + ", value=" + value); + final float val = Float.parseFloat(value); + lastAccount.addAmount(val, currency); + for (LedgerAccount syn : syntheticAccounts) { + L(String.format(Locale.ENGLISH, "propagating %s %1.2f to %s", + currency, val, syn.getName())); + syn.addAmount(val, currency); } + } - break; + if (match_found) { + syntheticAccounts.clear(); + state = ParserState.EXPECTING_ACCOUNT; + L("→ expecting account"); + } - case EXPECTING_TRANSACTION: - if (!line.isEmpty() && (line.charAt(0) == ' ')) - continue; - m = reTransactionStart.matcher(line); - if (m.find()) { - transactionId = Integer.valueOf(m.group(1)); - state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION; - L(String.format(Locale.ENGLISH, - "found transaction %d → expecting description", - transactionId)); - progress.setProgress(++processedTransactionCount); - if (maxTransactionId < transactionId) - maxTransactionId = transactionId; - if ((progress.getTotal() == Progress.INDETERMINATE) || - (progress.getTotal() < transactionId)) - progress.setTotal(transactionId); - publishProgress(progress); + break; + + case EXPECTING_TRANSACTION: + if (!line.isEmpty() && (line.charAt(0) == ' ')) + continue; + m = reTransactionStart.matcher(line); + if (m.find()) { + transactionId = Integer.parseInt(Objects.requireNonNull(m.group(1))); + state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION; + L(String.format(Locale.ENGLISH, + "found transaction %d → expecting description", transactionId)); + progress.setProgress(++processedTransactionCount); + if (maxTransactionId < transactionId) + maxTransactionId = transactionId; + if ((progress.isIndeterminate()) || + (progress.getTotal() < transactionId)) + progress.setTotal(transactionId); + publishProgress(progress); + } + m = reEnd.matcher(line); + if (m.find()) { + L("--- transaction value complete ---"); + break LINES; + } + break; + + case EXPECTING_TRANSACTION_DESCRIPTION: + if (!line.isEmpty() && (line.charAt(0) == ' ')) + continue; + m = reTransactionDescription.matcher(line); + if (m.find()) { + if (transactionId == 0) + throw new TransactionParserException( + "Transaction Id is 0 while expecting description"); + + String date = Objects.requireNonNull(m.group(1)); + try { + int equalsIndex = date.indexOf('='); + if (equalsIndex >= 0) + date = date.substring(equalsIndex + 1); + transaction = + new LedgerTransaction(transactionId, date, m.group(2)); } - m = reEnd.matcher(line); - if (m.find()) { - L("--- transaction value complete ---"); - break LINES; + catch (ParseException e) { + throw new TransactionParserException( + String.format("Error parsing date '%s'", date)); } - break; + state = ParserState.EXPECTING_TRANSACTION_DETAILS; + L(String.format(Locale.ENGLISH, + "transaction %d created for %s (%s) →" + " expecting details", + transactionId, date, m.group(2))); + } + break; - case EXPECTING_TRANSACTION_DESCRIPTION: - if (!line.isEmpty() && (line.charAt(0) == ' ')) - continue; - m = reTransactionDescription.matcher(line); - if (m.find()) { - if (transactionId == 0) - throw new TransactionParserException( - "Transaction Id is 0 while expecting " + "description"); - - String date = m.group(1); - try { - int equalsIndex = date.indexOf('='); - if (equalsIndex >= 0) - date = date.substring(equalsIndex + 1); - transaction = - new LedgerTransaction(transactionId, date, m.group(2)); - } - catch (ParseException e) { - e.printStackTrace(); - return String.format("Error parsing date '%s'", date); - } - state = ParserState.EXPECTING_TRANSACTION_DETAILS; - L(String.format(Locale.ENGLISH, - "transaction %d created for %s (%s) →" + - " expecting details", transactionId, date, m.group(2))); - } - break; - - case EXPECTING_TRANSACTION_DETAILS: - if (line.isEmpty()) { - // transaction data collected - if (transaction.existsInDb(db)) { - profile.markTransactionAsPresent(db, transaction); - matchedTransactionsCount++; - - if (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT) { - profile.markTransactionsBeforeTransactionAsPresent(db, - transaction); - progress.setTotal(progress.getProgress()); - publishProgress(progress); - break LINES; - } - } - else { - profile.storeTransaction(db, transaction); - matchedTransactionsCount = 0; - progress.setTotal(maxTransactionId); - } + case EXPECTING_TRANSACTION_DETAILS: + if (line.isEmpty()) { + // transaction data collected - state = ParserState.EXPECTING_TRANSACTION; - L(String.format("transaction %s saved → expecting transaction", - transaction.getId())); - transaction.finishLoading(); + transaction.finishLoading(); + transactions.add(transaction); + + state = ParserState.EXPECTING_TRANSACTION; + L(String.format("transaction %s parsed → expecting transaction", + transaction.getId())); // sounds like a good idea, but transaction-1 may not be the first one chronologically // for example, when you add the initial seeding transaction after entering some others @@ -400,142 +333,145 @@ public class RetrieveTransactionsTask // "parser"); // break LINES; // } + } + else { + LedgerTransactionAccount lta = parseTransactionAccountLine(line); + if (lta != null) { + transaction.addAccount(lta); + L(String.format(Locale.ENGLISH, "%d: %s = %s", transaction.getId(), + lta.getAccountName(), lta.getAmount())); } - else { - LedgerTransactionAccount lta = parseTransactionAccountLine(line); - if (lta != null) { - transaction.addAccount(lta); - L(String.format(Locale.ENGLISH, "%d: %s = %s", - transaction.getId(), lta.getAccountName(), - lta.getAmount())); - } - else - throw new IllegalStateException( - String.format("Can't parse transaction %d details: %s", - transactionId, line)); - } - break; - default: - throw new RuntimeException( - String.format("Unknown parser updating %s", state.name())); - } + else + throw new IllegalStateException( + String.format("Can't parse transaction %d details: %s", + transactionId, line)); + } + break; + default: + throw new RuntimeException( + String.format("Unknown parser updating %s", state.name())); } - - throwIfCancelled(); - - profile.deleteNotPresentTransactions(db); - db.setTransactionSuccessful(); - - profile.setLastUpdateStamp(); - - return null; - } - finally { - db.endTransaction(); } + + throwIfCancelled(); } } - private void prepareDbForRetrieval(SQLiteDatabase db, MobileLedgerProfile profile) { - db.execSQL("UPDATE transactions set keep=0 where profile=?", - new String[]{profile.getUuid()}); - db.execSQL("update account_values set keep=0 where profile=?;", - new String[]{profile.getUuid()}); - db.execSQL("update accounts set keep=0 where profile=?;", new String[]{profile.getUuid()}); - } - private boolean retrieveAccountList() throws IOException, HTTPException { - Progress progress = new Progress(); + private @NonNull + LedgerAccount ensureAccountExists(String accountName, HashMap map, + ArrayList createdAccounts) { + LedgerAccount acc = map.get(accountName); + + if (acc != null) + return acc; + + String parentName = LedgerAccount.extractParentName(accountName); + LedgerAccount parentAccount; + if (parentName != null) { + parentAccount = ensureAccountExists(parentName, map, createdAccounts); + } + else { + parentAccount = null; + } + acc = new LedgerAccount(profile, accountName, parentAccount); + createdAccounts.add(acc); + return acc; + } + private List retrieveAccountList() throws IOException, HTTPException { HttpURLConnection http = NetworkUtil.prepareConnection(profile, "accounts"); http.setAllowUserInteraction(false); switch (http.getResponseCode()) { case 200: break; case 404: - return false; + return null; default: throw new HTTPException(http.getResponseCode(), http.getResponseMessage()); } - publishProgress(progress); + publishProgress(Progress.indeterminate()); SQLiteDatabase db = App.getDatabase(); - ArrayList accountList = new ArrayList<>(); - boolean listFilledOK = false; + ArrayList list = new ArrayList<>(); + HashMap map = new HashMap<>(); + HashMap currentMap = new HashMap<>(); + for (LedgerAccount acc : prevAccounts) + currentMap.put(acc.getName(), acc); + throwIfCancelled(); try (InputStream resp = http.getInputStream()) { + throwIfCancelled(); if (http.getResponseCode() != 200) throw new IOException(String.format("HTTP error %d", http.getResponseCode())); - db.beginTransaction(); - try { - profile.markAccountsAsNotPresent(db); - - AccountListParser parser = new AccountListParser(resp); + AccountListParser parser = new AccountListParser(resp); + expectedPostingsCount = 0; - LedgerAccount prevAccount = null; + while (true) { + throwIfCancelled(); + ParsedLedgerAccount parsedAccount = parser.nextAccount(); + if (parsedAccount == null) { + break; + } + expectedPostingsCount += parsedAccount.getAnumpostings(); + final String accName = parsedAccount.getAname(); + LedgerAccount acc = map.get(accName); + if (acc != null) + throw new RuntimeException( + String.format("Account '%s' already present", acc.getName())); + String parentName = LedgerAccount.extractParentName(accName); + ArrayList createdParents = new ArrayList<>(); + LedgerAccount parent; + if (parentName == null) { + parent = null; + } + else { + parent = ensureAccountExists(parentName, map, createdParents); + parent.setHasSubAccounts(true); + } + acc = new LedgerAccount(profile, accName, parent); + list.add(acc); + map.put(accName, acc); - while (true) { + String lastCurrency = null; + float lastCurrencyAmount = 0; + for (ParsedBalance b : parsedAccount.getAibalance()) { throwIfCancelled(); - ParsedLedgerAccount parsedAccount = parser.nextAccount(); - if (parsedAccount == null) - break; - - LedgerAccount acc = profile.tryLoadAccount(db, parsedAccount.getAname()); - if (acc == null) - acc = new LedgerAccount(profile, parsedAccount.getAname()); - else - acc.removeAmounts(); - - profile.storeAccount(db, acc); - String lastCurrency = null; - float lastCurrencyAmount = 0; - for (ParsedBalance b : parsedAccount.getAibalance()) { - final String currency = b.getAcommodity(); - final float amount = b.getAquantity() - .asFloat(); - if (currency.equals(lastCurrency)) - lastCurrencyAmount += amount; - else { - if (lastCurrency != null) { - profile.storeAccountValue(db, acc.getName(), lastCurrency, - lastCurrencyAmount); - acc.addAmount(lastCurrencyAmount, lastCurrency); - } - lastCurrency = currency; - lastCurrencyAmount = amount; - } + final String currency = b.getAcommodity(); + final float amount = b.getAquantity() + .asFloat(); + if (currency.equals(lastCurrency)) { + lastCurrencyAmount += amount; } - if (lastCurrency != null) { - profile.storeAccountValue(db, acc.getName(), lastCurrency, - lastCurrencyAmount); - acc.addAmount(lastCurrencyAmount, lastCurrency); - } - - if (acc.isVisible(accountList)) - accountList.add(acc); - - if (prevAccount != null) { - prevAccount.setHasSubAccounts(acc.getName() - .startsWith(prevAccount.getName() + ":")); + else { + if (lastCurrency != null) { + acc.addAmount(lastCurrencyAmount, lastCurrency); + } + lastCurrency = currency; + lastCurrencyAmount = amount; } - - prevAccount = acc; } - throwIfCancelled(); - - profile.deleteNotPresentAccounts(db); - throwIfCancelled(); - db.setTransactionSuccessful(); - } - finally { - db.endTransaction(); + if (lastCurrency != null) { + acc.addAmount(lastCurrencyAmount, lastCurrency); + } + for (LedgerAccount p : createdParents) + acc.propagateAmountsTo(p); } + throwIfCancelled(); } - profile.setAccounts(accountList); + // the current account tree may have changed, update the new-to be tree to match + for (LedgerAccount acc : list) { + LedgerAccount prevData = currentMap.get(acc.getName()); + if (prevData != null) { + acc.setExpanded(prevData.isExpanded()); + acc.setAmountsExpanded(prevData.amountsExpanded()); + } + } - return true; + return list; } - private boolean retrieveTransactionList() throws IOException, ParseException, HTTPException { + private List retrieveTransactionList() + throws IOException, ParseException, HTTPException { Progress progress = new Progress(); - int maxTransactionId = Progress.INDETERMINATE; + progress.setTotal(expectedPostingsCount); HttpURLConnection http = NetworkUtil.prepareConnection(profile, "transactions"); http.setAllowUserInteraction(false); @@ -544,177 +480,196 @@ public class RetrieveTransactionsTask case 200: break; case 404: - return false; + return null; default: throw new HTTPException(http.getResponseCode(), http.getResponseMessage()); } - SQLiteDatabase db = App.getDatabase(); + ArrayList trList = new ArrayList<>(); try (InputStream resp = http.getInputStream()) { - if (http.getResponseCode() != 200) - throw new IOException(String.format("HTTP error %d", http.getResponseCode())); throwIfCancelled(); - db.beginTransaction(); - try { - profile.markTransactionsAsNotPresent(db); - - int matchedTransactionsCount = 0; - TransactionListParser parser = new TransactionListParser(resp); - - int processedTransactionCount = 0; - - DetectedTransactionOrder transactionOrder = DetectedTransactionOrder.UNKNOWN; - int orderAccumulator = 0; - int lastTransactionId = 0; - - while (true) { - throwIfCancelled(); - ParsedLedgerTransaction parsedTransaction = parser.nextTransaction(); - throwIfCancelled(); - if (parsedTransaction == null) - break; - - LedgerTransaction transaction = parsedTransaction.asLedgerTransaction(); - if (transaction.getId() > lastTransactionId) - orderAccumulator++; - else - orderAccumulator--; - lastTransactionId = transaction.getId(); - if (transactionOrder == DetectedTransactionOrder.UNKNOWN) { - if (orderAccumulator > 30) { - transactionOrder = DetectedTransactionOrder.FILE; - debug("rtt", String.format(Locale.ENGLISH, - "Detected native file order after %d transactions (factor %d)", - processedTransactionCount, orderAccumulator)); - progress.setTotal(Data.transactions.size()); - } - else if (orderAccumulator < -30) { - transactionOrder = DetectedTransactionOrder.REVERSE_CHRONOLOGICAL; - debug("rtt", String.format(Locale.ENGLISH, - "Detected reverse chronological order after %d transactions " + - "(factor %d)", processedTransactionCount, orderAccumulator)); - } - } - - if (transaction.existsInDb(db)) { - profile.markTransactionAsPresent(db, transaction); - matchedTransactionsCount++; - - if ((transactionOrder == DetectedTransactionOrder.REVERSE_CHRONOLOGICAL) && - (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT)) - { - profile.markTransactionsBeforeTransactionAsPresent(db, transaction); - progress.setTotal(progress.getProgress()); - publishProgress(progress); - db.setTransactionSuccessful(); - profile.setLastUpdateStamp(); - return true; - } - } - else { - profile.storeTransaction(db, transaction); - matchedTransactionsCount = 0; - progress.setTotal(maxTransactionId); - } + TransactionListParser parser = new TransactionListParser(resp); - if ((transactionOrder != DetectedTransactionOrder.UNKNOWN) && - ((progress.getTotal() == Progress.INDETERMINATE) || - (progress.getTotal() < transaction.getId()))) - progress.setTotal(transaction.getId()); - - progress.setProgress(++processedTransactionCount); - publishProgress(progress); - } + int processedPostings = 0; + while (true) { throwIfCancelled(); - profile.deleteNotPresentTransactions(db); + ParsedLedgerTransaction parsedTransaction = parser.nextTransaction(); throwIfCancelled(); - db.setTransactionSuccessful(); - profile.setLastUpdateStamp(); - } - finally { - db.endTransaction(); + if (parsedTransaction == null) + break; + + LedgerTransaction transaction = parsedTransaction.asLedgerTransaction(); + trList.add(transaction); + + progress.setProgress(processedPostings += transaction.getAccounts() + .size()); +// Logger.debug("trParser", +// String.format(Locale.US, "Parsed transaction %d - %s", transaction +// .getId(), +// transaction.getDescription())); +// for (LedgerTransactionAccount acc : transaction.getAccounts()) { +// Logger.debug("trParser", +// String.format(Locale.US, " %s", acc.getAccountName())); +// } + publishProgress(progress); } + + throwIfCancelled(); } - return true; + // json interface returns transactions if file order and the rest of the machinery + // expects them in reverse chronological order + Collections.sort(trList, (o1, o2) -> { + int res = o2.getDate() + .compareTo(o1.getDate()); + if (res != 0) + return res; + return Integer.compare(o2.getId(), o1.getId()); + }); + return trList; } @SuppressLint("DefaultLocale") @Override - protected String doInBackground(Void... params) { + protected Result doInBackground(Void... params) { Data.backgroundTaskStarted(); + List accounts; + List transactions; try { - if (!retrieveAccountList() || !retrieveTransactionList()) - return retrieveTransactionListLegacy(); - return null; + accounts = retrieveAccountList(); + if (accounts == null) + transactions = null; + else + transactions = retrieveTransactionList(); + if (accounts == null || transactions == null) { + accounts = new ArrayList<>(); + transactions = new ArrayList<>(); + retrieveTransactionListLegacy(accounts, transactions); + } + mainModel.setAndStoreAccountAndTransactionListFromWeb(accounts, transactions); + + return new Result(accounts, transactions); } catch (MalformedURLException e) { e.printStackTrace(); - return "Invalid server URL"; + return new Result("Invalid server URL"); } catch (HTTPException e) { e.printStackTrace(); - return String.format("HTTP error %d: %s", e.getResponseCode(), e.getResponseMessage()); + return new Result(String.format("HTTP error %d: %s", e.getResponseCode(), + e.getResponseMessage())); } catch (IOException e) { e.printStackTrace(); - return e.getLocalizedMessage(); + return new Result(e.getLocalizedMessage()); } catch (ParseException e) { e.printStackTrace(); - return "Network error"; + return new Result("Network error"); } catch (OperationCanceledException e) { e.printStackTrace(); - return "Operation cancelled"; + return new Result("Operation cancelled"); } finally { Data.backgroundTaskFinished(); } } - private MainActivity getContext() { - return contextRef.get(); - } private void throwIfCancelled() { if (isCancelled()) throw new OperationCanceledException(null); } - enum DetectedTransactionOrder {UNKNOWN, REVERSE_CHRONOLOGICAL, FILE} - private enum ParserState { - EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_JOURNAL, EXPECTING_TRANSACTION, + EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_TRANSACTION, EXPECTING_TRANSACTION_DESCRIPTION, EXPECTING_TRANSACTION_DETAILS } - public class Progress { - public static final int INDETERMINATE = -1; + public enum ProgressState {STARTING, RUNNING, FINISHED} + + public static class Progress { private int progress; private int total; + private ProgressState state = ProgressState.RUNNING; + private String error = null; + private boolean indeterminate; Progress() { - this(INDETERMINATE, INDETERMINATE); + indeterminate = true; } Progress(int progress, int total) { + this.indeterminate = false; this.progress = progress; this.total = total; } + public static Progress indeterminate() { + return new Progress(); + } + public static Progress finished(String error) { + Progress p = new Progress(); + p.setState(ProgressState.FINISHED); + p.setError(error); + return p; + } public int getProgress() { + ensureState(ProgressState.RUNNING); return progress; } protected void setProgress(int progress) { this.progress = progress; + this.state = ProgressState.RUNNING; } public int getTotal() { + ensureState(ProgressState.RUNNING); return total; } protected void setTotal(int total) { this.total = total; + state = ProgressState.RUNNING; + indeterminate = total == -1; + } + private void ensureState(ProgressState wanted) { + if (state != wanted) + throw new IllegalStateException( + String.format("Bad state: %s, expected %s", state, wanted)); + } + public ProgressState getState() { + return state; + } + public void setState(ProgressState state) { + this.state = state; + } + public String getError() { + ensureState(ProgressState.FINISHED); + return error; + } + public void setError(String error) { + this.error = error; + state = ProgressState.FINISHED; + } + public boolean isIndeterminate() { + return indeterminate; + } + public void setIndeterminate(boolean indeterminate) { + this.indeterminate = indeterminate; } } - private class TransactionParserException extends IllegalStateException { + private static class TransactionParserException extends IllegalStateException { TransactionParserException(String message) { super(message); } } + + public static class Result { + public String error; + public List accounts; + public List transactions; + Result(String error) { + this.error = error; + } + Result(List accounts, List transactions) { + this.accounts = accounts; + this.transactions = transactions; + } + } }