]> git.ktnx.net Git - mobile-ledger.git/blobdiff - app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java
more pronounced day/month delimiters in the transaction list
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / async / RetrieveTransactionsTask.java
index 85f7665146b556303edfbe8435738b740394e50b..0b751b4c6d7df0c94174973ec7a2e449352c9dc9 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
 package net.ktnx.mobileledger.async;
 
 import android.annotation.SuppressLint;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
 import android.os.OperationCanceledException;
 
 import androidx.annotation.NonNull;
 
-import net.ktnx.mobileledger.App;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
+
+import net.ktnx.mobileledger.dao.AccountDAO;
+import net.ktnx.mobileledger.dao.TransactionDAO;
+import net.ktnx.mobileledger.db.Account;
+import net.ktnx.mobileledger.db.AccountWithAmounts;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Option;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
 import net.ktnx.mobileledger.err.HTTPException;
-import net.ktnx.mobileledger.json.v1_15.AccountListParser;
-import net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction;
-import net.ktnx.mobileledger.json.v1_15.TransactionListParser;
+import net.ktnx.mobileledger.json.API;
+import net.ktnx.mobileledger.json.AccountListParser;
+import net.ktnx.mobileledger.json.ApiNotSupportedException;
+import net.ktnx.mobileledger.json.TransactionListParser;
 import net.ktnx.mobileledger.model.Data;
 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.MainModel;
+import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.NetworkUtil;
 
 import java.io.BufferedReader;
@@ -48,6 +56,7 @@ import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -56,8 +65,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 
-public class RetrieveTransactionsTask extends
-        AsyncTask<Void, RetrieveTransactionsTask.Progress, RetrieveTransactionsTask.Result> {
+public class RetrieveTransactionsTask extends Thread {
     private static final int MATCHING_TRANSACTIONS_LIMIT = 150;
     private static final Pattern reComment = Pattern.compile("^\\s*;");
     private static final Pattern reTransactionStart = Pattern.compile(
@@ -71,21 +79,16 @@ public class RetrieveTransactionsTask extends
     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 static final String TAG = "RTT";
     // %3A is '='
     private final Pattern reAccountName =
             Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
     private final Pattern reAccountValue = Pattern.compile(
             "<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
-    private final MainModel mainModel;
-    private final MobileLedgerProfile profile;
-    private final List<LedgerAccount> prevAccounts;
+    private final Profile profile;
     private int expectedPostingsCount = -1;
-    public RetrieveTransactionsTask(@NonNull MainModel mainModel,
-                                    @NonNull MobileLedgerProfile profile,
-                                    List<LedgerAccount> accounts) {
-        this.mainModel = mainModel;
+    public RetrieveTransactionsTask(@NonNull Profile profile) {
         this.profile = profile;
-        this.prevAccounts = accounts;
     }
     private static void L(String msg) {
         //debug("transaction-parser", msg);
@@ -117,28 +120,19 @@ public class RetrieveTransactionsTask extends
             return null;
         }
     }
-    public MobileLedgerProfile getProfile() {
-        return profile;
+    private void publishProgress(Progress progress) {
+        Data.backgroundTaskProgress.postValue(progress);
     }
-    @Override
-    protected void onProgressUpdate(Progress... values) {
-        super.onProgressUpdate(values);
-        Data.backgroundTaskProgress.postValue(values[0]);
-    }
-    @Override
-    protected void onPostExecute(Result result) {
-        super.onPostExecute(result);
+    private void finish(Result result) {
         Progress progress = new Progress();
         progress.setState(ProgressState.FINISHED);
         progress.setError(result.error);
-        onProgressUpdate(progress);
+        publishProgress(progress);
     }
-    @Override
-    protected void onCancelled() {
-        super.onCancelled();
+    private void cancel() {
         Progress progress = new Progress();
         progress.setState(ProgressState.FINISHED);
-        onProgressUpdate(progress);
+        publishProgress(progress);
     }
     private void retrieveTransactionListLegacy(List<LedgerAccount> accounts,
                                                List<LedgerTransaction> transactions)
@@ -210,7 +204,7 @@ public class RetrieveTransactionsTask extends
                             else {
                                 parentAccount = null;
                             }
-                            lastAccount = new LedgerAccount(profile, accName, parentAccount);
+                            lastAccount = new LedgerAccount(accName, parentAccount);
 
                             accounts.add(lastAccount);
                             map.put(accName, lastAccount);
@@ -324,7 +318,7 @@ public class RetrieveTransactionsTask extends
 
                             state = ParserState.EXPECTING_TRANSACTION;
                             L(String.format("transaction %s parsed → expecting transaction",
-                                    transaction.getId()));
+                                    transaction.getLedgerId()));
 
 // 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
@@ -339,8 +333,9 @@ public class RetrieveTransactionsTask extends
                             LedgerTransactionAccount lta = parseTransactionAccountLine(line);
                             if (lta != null) {
                                 transaction.addAccount(lta);
-                                L(String.format(Locale.ENGLISH, "%d: %s = %s", transaction.getId(),
-                                        lta.getAccountName(), lta.getAmount()));
+                                L(String.format(Locale.ENGLISH, "%d: %s = %s",
+                                        transaction.getLedgerId(), lta.getAccountName(),
+                                        lta.getAmount()));
                             }
                             else
                                 throw new IllegalStateException(
@@ -374,14 +369,46 @@ public class RetrieveTransactionsTask extends
             parentAccount = null;
         }
 
-        acc = new LedgerAccount(profile, accountName, parentAccount);
+        acc = new LedgerAccount(accountName, parentAccount);
         createdAccounts.add(acc);
         return acc;
     }
     public void addNumberOfPostings(int number) {
         expectedPostingsCount += number;
     }
-    private List<LedgerAccount> retrieveAccountList() throws IOException, HTTPException {
+    private List<LedgerAccount> retrieveAccountList()
+            throws IOException, HTTPException, ApiNotSupportedException {
+        final API apiVersion = API.valueOf(profile.getApiVersion());
+        if (apiVersion.equals(API.auto)) {
+            return retrieveAccountListAnyVersion();
+        }
+        else if (apiVersion.equals(API.html)) {
+            Logger.debug("json",
+                    "Declining using JSON API for /accounts with configured legacy API version");
+            return null;
+        }
+        else {
+            return retrieveAccountListForVersion(apiVersion);
+        }
+    }
+    private List<LedgerAccount> retrieveAccountListAnyVersion()
+            throws ApiNotSupportedException, IOException, HTTPException {
+        for (API ver : API.allVersions) {
+            try {
+                return retrieveAccountListForVersion(ver);
+            }
+            catch (JsonParseException | RuntimeJsonMappingException e) {
+                Logger.debug("json",
+                        String.format(Locale.US, "Error during account list retrieval using API %s",
+                                ver.getDescription()), e);
+            }
+
+        }
+
+        throw new ApiNotSupportedException();
+    }
+    private List<LedgerAccount> retrieveAccountListForVersion(API version)
+            throws IOException, HTTPException {
         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "accounts");
         http.setAllowUserInteraction(false);
         switch (http.getResponseCode()) {
@@ -393,43 +420,66 @@ public class RetrieveTransactionsTask extends
                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
         }
         publishProgress(Progress.indeterminate());
-        SQLiteDatabase db = App.getDatabase();
         ArrayList<LedgerAccount> list = new ArrayList<>();
         HashMap<String, LedgerAccount> map = new HashMap<>();
-        HashMap<String, LedgerAccount> 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()));
 
-            AccountListParser parser = new AccountListParser(resp);
+            AccountListParser parser = AccountListParser.forApiVersion(version, resp);
             expectedPostingsCount = 0;
 
             while (true) {
                 throwIfCancelled();
-                LedgerAccount acc = parser.nextLedgerAccount(this, map);
+                LedgerAccount acc = parser.nextAccount(this, map);
                 if (acc == null)
                     break;
                 list.add(acc);
             }
             throwIfCancelled();
-        }
 
-        // 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());
-            }
+            Logger.warn("accounts",
+                    String.format(Locale.US, "Got %d accounts using protocol %s", list.size(),
+                            version.getDescription()));
         }
 
         return list;
     }
     private List<LedgerTransaction> retrieveTransactionList()
+            throws ParseException, HTTPException, IOException, ApiNotSupportedException {
+        final API apiVersion = API.valueOf(profile.getApiVersion());
+        if (apiVersion.equals(API.auto)) {
+            return retrieveTransactionListAnyVersion();
+        }
+        else if (apiVersion.equals(API.html)) {
+            Logger.debug("json",
+                    "Declining using JSON API for /accounts with configured legacy API version");
+            return null;
+        }
+        else {
+            return retrieveTransactionListForVersion(apiVersion);
+        }
+
+    }
+    private List<LedgerTransaction> retrieveTransactionListAnyVersion()
+            throws ApiNotSupportedException {
+        for (API ver : API.allVersions) {
+            try {
+                return retrieveTransactionListForVersion(ver);
+            }
+            catch (Exception e) {
+                Logger.debug("json", String.format(Locale.US,
+                        "Error during transaction list retrieval using API %s",
+                        ver.getDescription()), e);
+            }
+
+        }
+
+        throw new ApiNotSupportedException();
+    }
+    private List<LedgerTransaction> retrieveTransactionListForVersion(API apiVersion)
             throws IOException, ParseException, HTTPException {
         Progress progress = new Progress();
         progress.setTotal(expectedPostingsCount);
@@ -449,18 +499,17 @@ public class RetrieveTransactionsTask extends
         try (InputStream resp = http.getInputStream()) {
             throwIfCancelled();
 
-            TransactionListParser parser = new TransactionListParser(resp);
+            TransactionListParser parser = TransactionListParser.forApiVersion(apiVersion, resp);
 
             int processedPostings = 0;
 
             while (true) {
                 throwIfCancelled();
-                ParsedLedgerTransaction parsedTransaction = parser.nextTransaction();
+                LedgerTransaction transaction = parser.nextTransaction();
                 throwIfCancelled();
-                if (parsedTransaction == null)
+                if (transaction == null)
                     break;
 
-                LedgerTransaction transaction = parsedTransaction.asLedgerTransaction();
                 trList.add(transaction);
 
                 progress.setProgress(processedPostings += transaction.getAccounts()
@@ -477,68 +526,88 @@ public class RetrieveTransactionsTask extends
             }
 
             throwIfCancelled();
+
+            Logger.warn("transactions",
+                    String.format(Locale.US, "Got %d transactions using protocol %s", trList.size(),
+                            apiVersion.getDescription()));
         }
 
-        // json interface returns transactions if file order and the rest of the machinery
+        // json interface returns transactions in 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 Long.compare(o2.getLedgerId(), o1.getLedgerId());
         });
         return trList;
     }
 
     @SuppressLint("DefaultLocale")
     @Override
-    protected Result doInBackground(Void... params) {
+    public void run() {
         Data.backgroundTaskStarted();
         List<LedgerAccount> accounts;
         List<LedgerTransaction> transactions;
         try {
             accounts = retrieveAccountList();
+            // accounts is null in API-version auto-detection and means
+            // requesting 'html' API version via the JSON classes
+            // this can't work, and the null results in the legacy code below
+            // being called
             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);
+            new AccountAndTransactionListSaver(accounts, transactions).start();
+
+            Data.lastUpdateDate.postValue(new Date());
+
+            finish(new Result(null));
         }
         catch (MalformedURLException e) {
             e.printStackTrace();
-            return new Result("Invalid server URL");
+            finish(new Result("Invalid server URL"));
         }
         catch (HTTPException e) {
             e.printStackTrace();
-            return new Result(String.format("HTTP error %d: %s", e.getResponseCode(),
-                    e.getResponseMessage()));
+            finish(new Result(
+                    String.format("HTTP error %d: %s", e.getResponseCode(), e.getMessage())));
         }
         catch (IOException e) {
             e.printStackTrace();
-            return new Result(e.getLocalizedMessage());
+            finish(new Result(e.getLocalizedMessage()));
+        }
+        catch (RuntimeJsonMappingException e) {
+            e.printStackTrace();
+            finish(new Result(Result.ERR_JSON_PARSER_ERROR));
         }
         catch (ParseException e) {
             e.printStackTrace();
-            return new Result("Network error");
+            finish(new Result("Network error"));
         }
         catch (OperationCanceledException e) {
+            Logger.debug("RTT", "Retrieval was cancelled", e);
+            finish(new Result(null));
+        }
+        catch (ApiNotSupportedException e) {
             e.printStackTrace();
-            return new Result("Operation cancelled");
+            finish(new Result("Server version not supported"));
         }
         finally {
             Data.backgroundTaskFinished();
         }
     }
     public void throwIfCancelled() {
-        if (isCancelled())
+        if (isInterrupted())
             throw new OperationCanceledException(null);
     }
     private enum ParserState {
@@ -622,6 +691,7 @@ public class RetrieveTransactionsTask extends
     }
 
     public static class Result {
+        public static String ERR_JSON_PARSER_ERROR = "err_json_parser";
         public String error;
         public List<LedgerAccount> accounts;
         public List<LedgerTransaction> transactions;
@@ -633,4 +703,55 @@ public class RetrieveTransactionsTask extends
             this.transactions = transactions;
         }
     }
+
+    private class AccountAndTransactionListSaver extends Thread {
+        private final List<LedgerAccount> accounts;
+        private final List<LedgerTransaction> transactions;
+        public AccountAndTransactionListSaver(List<LedgerAccount> accounts,
+                                              List<LedgerTransaction> transactions) {
+            this.accounts = accounts;
+            this.transactions = transactions;
+        }
+        @Override
+        public void run() {
+            AccountDAO accDao = DB.get()
+                                  .getAccountDAO();
+            TransactionDAO trDao = DB.get()
+                                     .getTransactionDAO();
+
+            Logger.debug(TAG, "Preparing account list");
+            final List<AccountWithAmounts> list = new ArrayList<>();
+            for (LedgerAccount acc : accounts) {
+                final AccountWithAmounts a = acc.toDBOWithAmounts();
+                Account existing = accDao.getByNameSync(profile.getId(), acc.getName());
+                if (existing != null) {
+                    a.account.setExpanded(existing.isExpanded());
+                    a.account.setAmountsExpanded(existing.isAmountsExpanded());
+                    a.account.setId(existing.getId()); // not strictly needed, but since we have it
+                    // anyway...
+                }
+
+                list.add(a);
+            }
+            Logger.debug(TAG, "Account list prepared. Storing");
+            accDao.storeAccountsSync(list, profile.getId());
+            Logger.debug(TAG, "Account list stored");
+
+            Logger.debug(TAG, "Preparing transaction list");
+            final List<TransactionWithAccounts> tranList = new ArrayList<>();
+
+            for (LedgerTransaction tr : transactions)
+                tranList.add(tr.toDBO());
+
+            Logger.debug(TAG, "Storing transaction list");
+            trDao.storeTransactionsSync(tranList, profile.getId());
+
+            Logger.debug(TAG, "Transactions stored");
+
+            DB.get()
+              .getOptionDAO()
+              .insertSync(new Option(profile.getId(), Option.OPT_LAST_SCRAPE,
+                      String.valueOf((new Date()).getTime())));
+        }
+    }
 }