]> git.ktnx.net Git - mobile-ledger.git/commitdiff
machinery for retrieving transaction journal from hledger-web
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 14 Dec 2018 18:26:38 +0000 (18:26 +0000)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Fri, 14 Dec 2018 18:26:38 +0000 (18:26 +0000)
app/src/main/java/net/ktnx/mobileledger/LedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/LedgerTransactionItem.java
app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java
app/src/main/java/net/ktnx/mobileledger/NetworkUtil.java
app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java
app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/SaveTransactionTask.java
app/src/main/res/raw/sql_7.sql [new file with mode: 0644]

index 3000b2e35ecd20a6c634a828e94235847cbde858..ad456cef740873d4ab7c2f2e5c5a6268292416d9 100644 (file)
 
 package net.ktnx.mobileledger;
 
+import android.database.sqlite.SQLiteDatabase;
+
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 
 class LedgerTransaction {
+    private String id;
     private String date;
     private String description;
     private List<LedgerTransactionItem> items;
 
-    LedgerTransaction(String date, String description) {
+    LedgerTransaction(String id, String date, String description) {
+        this.id = id;
         this.date = date;
         this.description = description;
         this.items = new ArrayList<>();
     }
-
+    LedgerTransaction(String date, String description) {
+        this(null, date, description);
+    }
     void add_item(LedgerTransactionItem item) {
         items.add(item);
     }
@@ -66,4 +72,18 @@ class LedgerTransaction {
             }
         };
     }
+    public String getId() {
+        return id;
+    }
+
+    void insertInto(SQLiteDatabase db) {
+        db.execSQL("INSERT INTO transactions(id, date, " + "description) values(?, ?, ?)",
+                new String[]{id, date, description});
+
+        for(LedgerTransactionItem item : items) {
+            db.execSQL("INSERT INTO transaction_accounts(transaction_id, account_name, amount, "
+                    + "currency) values(?, ?, ?, ?)", new Object[]{id, item.getAccountName(),
+                                                                   item.getAmount(), item.getCurrency()});
+        }
+    }
 }
index e84e1c779fb5cb22745b2f0d0b6377eabf6a2077..3453a617cac1112242b8a211cf7beae32ca192c5 100644 (file)
 package net.ktnx.mobileledger;
 
 class LedgerTransactionItem {
-    private String account_name;
+    private String accountName;
     private float amount;
-    private boolean amount_set;
+    private boolean amountSet;
+    private String currency;
 
-    LedgerTransactionItem(String account_name, float amount) {
-        this.account_name = account_name;
+    LedgerTransactionItem(String accountName, float amount) {
+        this(accountName, amount, null);
+    }
+    LedgerTransactionItem(String accountName, float amount, String currency) {
+        this.accountName = accountName;
         this.amount = amount;
-        this.amount_set = true;
+        this.amountSet = true;
+        this.currency = currency;
     }
 
-    public LedgerTransactionItem(String account_name) {
-        this.account_name = account_name;
+    public LedgerTransactionItem(String accountName) {
+        this.accountName = accountName;
     }
 
-    public String get_account_name() {
-        return account_name;
+    public String getAccountName() {
+        return accountName;
     }
 
-    public void set_account_name(String account_name) {
-        this.account_name = account_name;
+    public void setAccountName(String accountName) {
+        this.accountName = accountName;
     }
 
-    public float get_amount() {
-        if (!amount_set)
+    public float getAmount() {
+        if (!amountSet)
             throw new IllegalStateException("Account amount is not set");
 
         return amount;
     }
 
-    public void set_amount(float account_amount) {
+    public void setAmount(float account_amount) {
         this.amount = account_amount;
-        this.amount_set = true;
+        this.amountSet = true;
     }
 
-    public void reset_amount() {
-        this.amount_set = false;
+    public void resetAmount() {
+        this.amountSet = false;
     }
 
-    public boolean is_amount_set() {
-        return amount_set;
+    public boolean isAmountSet() {
+        return amountSet;
+    }
+    public String getCurrency() {
+        return currency;
     }
 }
index 91de7352e5c84837f75651686fa4bc1d0fbdd3f9..c7f4c5ee79028ce2c3309d55d7ca06d7a717577a 100644 (file)
@@ -35,7 +35,7 @@ class MobileLedgerDatabase extends SQLiteOpenHelper implements AutoCloseable {
     static final String DB_NAME = "mobile-ledger.db";
     static final String ACCOUNTS_TABLE = "accounts";
     static final String DESCRIPTION_HISTORY_TABLE = "description_history";
-    static final int LATEST_REVISION = 6;
+    static final int LATEST_REVISION = 7;
 
     final Context mContext;
 
index 1de43aed941489d38f13caba6da2411f629e925d..e311fcf9e6936b10a8a09163eaeb98635354726f 100644 (file)
@@ -39,6 +39,7 @@ final class NetworkUtil {
             http.setRequestProperty("Authorization", "Basic " + value);
         }
         http.setAllowUserInteraction(false);
+        http.setRequestProperty("Accept-Charset", "UTF-8");
         http.setInstanceFollowRedirects(false);
         http.setUseCaches(false);
 
index 8532e1ac2f270c25dbb7d155fe4d71a734106817..6e66453b38a6021bd54b8096d147db63e16c7e1e 100644 (file)
@@ -51,8 +51,6 @@ class RetrieveAccountsTask extends android.os.AsyncTask<Void, Integer, Void> {
     protected Void doInBackground(Void... params) {
         try {
             HttpURLConnection http = NetworkUtil.prepare_connection( pref, "add");
-            http.setAllowUserInteraction(false);
-            http.setRequestProperty("Accept-Charset", "UTF-8");
             publishProgress(0);
             try(MobileLedgerDatabase dbh = new MobileLedgerDatabase(mContext.get())) {
                 try(SQLiteDatabase db = dbh.getWritableDatabase()) {
diff --git a/app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java
new file mode 100644 (file)
index 0000000..4131a21
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * Copyright © 2018 Damyan Ivanov.
+ * This file is part of Mobile-Ledger.
+ * Mobile-Ledger is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * Mobile-Ledger is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.AsyncTask;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+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.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class Params {
+    static final int DEFAULT_LIMIT = 100;
+    private SharedPreferences backendPref;
+    private String accountsRoot;
+    private int limit;
+
+    Params(SharedPreferences backendPref) {
+        this.backendPref = backendPref;
+        this.accountsRoot = null;
+        this.limit = DEFAULT_LIMIT;
+    }
+    Params(SharedPreferences backendPref, String accountsRoot) {
+        this(backendPref, accountsRoot, DEFAULT_LIMIT);
+    }
+    Params(SharedPreferences backendPref, String accountsRoot, int limit) {
+        this.backendPref = backendPref;
+        this.accountsRoot = accountsRoot;
+        this.limit = limit;
+    }
+    String getAccountsRoot() {
+        return accountsRoot;
+    }
+    SharedPreferences getBackendPref() {
+        return backendPref;
+    }
+    int getLimit() {
+        return limit;
+    }
+}
+
+class RetrieveTransactionsTask extends AsyncTask<Params, Integer, Void> {
+    private static final Pattern transactionStartPattern = Pattern.compile("<tr class=\"title\" "
+            + "id=\"transaction-(\\d+)\"><td class=\"date\"[^\\\"]*>([\\d.-]+)</td>");
+    private static final Pattern transactionDescriptionPattern =
+            Pattern.compile("<tr class=\"posting\" title=\"(\\S+)\\s(.+)");
+    private static final Pattern transactionDetailsPattern =
+            Pattern.compile("^\\s+" + "(\\S[\\S\\s]+\\S)\\s\\s+([-+]?\\d[\\d,.]*)");
+    protected WeakReference<Context> contextRef;
+    protected int error;
+    @Override
+    protected Void doInBackground(Params... params) {
+        try {
+            HttpURLConnection http =
+                    NetworkUtil.prepare_connection(params[0].getBackendPref(), "journal");
+            http.setAllowUserInteraction(false);
+            publishProgress(0);
+            Context ctx = contextRef.get();
+            if (ctx == null) return null;
+            try (MobileLedgerDatabase dbh = new MobileLedgerDatabase(ctx)) {
+                try (SQLiteDatabase db = dbh.getWritableDatabase()) {
+                    try (InputStream resp = http.getInputStream()) {
+                        if (http.getResponseCode() != 200) throw new IOException(
+                                String.format("HTTP error %d", http.getResponseCode()));
+                        db.beginTransaction();
+                        try {
+                            String root = params[0].getAccountsRoot();
+                            if (root == null) db.execSQL("DELETE FROM transaction_history;");
+                            else {
+                                StringBuilder sql = new StringBuilder();
+                                sql.append("DELETE FROM transaction_history ");
+                                sql.append(
+                                        "where id in (select transactions.id from transactions ");
+                                sql.append("join transaction_accounts ");
+                                sql.append(
+                                        "on transactions.id=transaction_accounts.transaction_id ");
+                                sql.append("where transaction_accounts.account_name like ?||'%'");
+                                db.execSQL(sql.toString(), new String[]{root});
+                            }
+
+                            int state = ParserState.EXPECTING_JOURNAL;
+                            String line;
+                            BufferedReader buf =
+                                    new BufferedReader(new InputStreamReader(resp, "UTF-8"));
+
+                            int transactionCount = 0;
+                            String transactionId = null;
+                            LedgerTransaction transaction = null;
+                            while ((line = buf.readLine()) != null) {
+                                switch (state) {
+                                    case ParserState.EXPECTING_JOURNAL: {
+                                        if (line.equals("<h2>General Journal</h2>"))
+                                            state = ParserState.EXPECTING_TRANSACTION;
+                                        continue;
+                                    }
+                                    case ParserState.EXPECTING_TRANSACTION: {
+                                        Matcher m = transactionStartPattern.matcher(line);
+                                        if (m.find()) {
+                                            transactionId = m.group(1);
+                                            state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
+                                        }
+                                    }
+                                    case ParserState.EXPECTING_TRANSACTION_DESCRIPTION: {
+                                        Matcher m = transactionDescriptionPattern.matcher(line);
+                                        if (m.find()) {
+                                            if (transactionId == null)
+                                                throw new TransactionParserException(
+                                                        "Transaction Id is null while expecting description");
+
+                                            transaction =
+                                                    new LedgerTransaction(transactionId, m.group(1),
+                                                            m.group(2));
+                                            state = ParserState.EXPECTING_TRANSACTION_DETAILS;
+                                        }
+                                    }
+                                    case ParserState.EXPECTING_TRANSACTION_DETAILS: {
+                                        if (transaction == null)
+                                            throw new TransactionParserException(
+                                                    "Transaction is null while expecting details");
+                                        if (line.isEmpty()) {
+                                            // transaction data collected
+                                            transaction.insertInto(db);
+
+                                            state = ParserState.EXPECTING_TRANSACTION;
+                                            publishProgress(++transactionCount);
+                                        }
+                                        else {
+                                            Matcher m = transactionDetailsPattern.matcher(line);
+                                            if (m.find()) {
+                                                String acc_name = m.group(1);
+                                                String amount = m.group(2);
+                                                amount = amount.replace(',', '.');
+                                                transaction.add_item(
+                                                        new LedgerTransactionItem(acc_name,
+                                                                Float.valueOf(amount)));
+                                            }
+                                            else throw new IllegalStateException(String.format(
+                                                    "Can't" + " parse transaction details"));
+                                        }
+                                    }
+                                    default:
+                                        throw new RuntimeException(
+                                                String.format("Unknown " + "parser state %d",
+                                                        state));
+                                }
+                            }
+                            db.setTransactionSuccessful();
+                        }
+                        finally {
+                            db.endTransaction();
+                        }
+                    }
+                }
+            }
+        }
+        catch (MalformedURLException e) {
+            error = R.string.err_bad_backend_url;
+            e.printStackTrace();
+        }
+        catch (FileNotFoundException e) {
+            error = R.string.err_bad_auth;
+            e.printStackTrace();
+        }
+        catch (IOException e) {
+            error = R.string.err_net_io_error;
+            e.printStackTrace();
+        }
+        return null;
+    }
+    WeakReference<Context> getContextRef() {
+        return contextRef;
+    }
+
+    private class TransactionParserException extends IllegalStateException {
+        TransactionParserException(String message) {
+            super(message);
+        }
+    }
+
+    private class ParserState {
+        static final int EXPECTING_JOURNAL = 0;
+        static final int EXPECTING_TRANSACTION = 1;
+        static final int EXPECTING_TRANSACTION_DESCRIPTION = 2;
+        static final int EXPECTING_TRANSACTION_DETAILS = 3;
+    }
+}
index 6ebfc511e0f1151a3e8880f9b9bdf5ae0e99e266..2e95a7d49b5f1cec0d381b3e6ef181325bf257b7 100644 (file)
@@ -72,9 +72,9 @@ class SaveTransactionTask extends AsyncTask<LedgerTransaction, Void, Void> {
             Iterator<LedgerTransactionItem> items = ltr.getItemsIterator();
             while (items.hasNext()) {
                 LedgerTransactionItem item = items.next();
-                params.add_pair("account", item.get_account_name());
-                if (item.is_amount_set())
-                    params.add_pair("amount", String.format(Locale.US, "%1.2f", item.get_amount()));
+                params.add_pair("account", item.getAccountName());
+                if (item.isAmountSet())
+                    params.add_pair("amount", String.format(Locale.US, "%1.2f", item.getAmount()));
                 else params.add_pair("amount", "");
             }
         }
diff --git a/app/src/main/res/raw/sql_7.sql b/app/src/main/res/raw/sql_7.sql
new file mode 100644 (file)
index 0000000..5217e5d
--- /dev/null
@@ -0,0 +1,2 @@
+create table transactions(id varchar primary key, date varchar, description varchar);
+create table transaction_accounts(transaction_id integer not null, account_name varchar not null, amount float, currency varchar, foreign key (transaction_id) references transactions(id), foreign key(account_name) references accounts(name));
\ No newline at end of file