From 674b18d882411b94513d77c0aea39fd929a7a62b Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Fri, 14 Dec 2018 18:26:38 +0000 Subject: [PATCH] machinery for retrieving transaction journal from hledger-web --- .../ktnx/mobileledger/LedgerTransaction.java | 24 +- .../mobileledger/LedgerTransactionItem.java | 46 ++-- .../mobileledger/MobileLedgerDatabase.java | 2 +- .../net/ktnx/mobileledger/NetworkUtil.java | 1 + .../mobileledger/RetrieveAccountsTask.java | 2 - .../RetrieveTransactionsTask.java | 210 ++++++++++++++++++ .../mobileledger/SaveTransactionTask.java | 6 +- app/src/main/res/raw/sql_7.sql | 2 + 8 files changed, 266 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java create mode 100644 app/src/main/res/raw/sql_7.sql diff --git a/app/src/main/java/net/ktnx/mobileledger/LedgerTransaction.java b/app/src/main/java/net/ktnx/mobileledger/LedgerTransaction.java index 3000b2e3..ad456cef 100644 --- a/app/src/main/java/net/ktnx/mobileledger/LedgerTransaction.java +++ b/app/src/main/java/net/ktnx/mobileledger/LedgerTransaction.java @@ -17,21 +17,27 @@ 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 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()}); + } + } } diff --git a/app/src/main/java/net/ktnx/mobileledger/LedgerTransactionItem.java b/app/src/main/java/net/ktnx/mobileledger/LedgerTransactionItem.java index e84e1c77..3453a617 100644 --- a/app/src/main/java/net/ktnx/mobileledger/LedgerTransactionItem.java +++ b/app/src/main/java/net/ktnx/mobileledger/LedgerTransactionItem.java @@ -18,45 +18,53 @@ 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; } } diff --git a/app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java b/app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java index 91de7352..c7f4c5ee 100644 --- a/app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java +++ b/app/src/main/java/net/ktnx/mobileledger/MobileLedgerDatabase.java @@ -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; diff --git a/app/src/main/java/net/ktnx/mobileledger/NetworkUtil.java b/app/src/main/java/net/ktnx/mobileledger/NetworkUtil.java index 1de43aed..e311fcf9 100644 --- a/app/src/main/java/net/ktnx/mobileledger/NetworkUtil.java +++ b/app/src/main/java/net/ktnx/mobileledger/NetworkUtil.java @@ -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); diff --git a/app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java b/app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java index 8532e1ac..6e66453b 100644 --- a/app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/RetrieveAccountsTask.java @@ -51,8 +51,6 @@ class RetrieveAccountsTask extends android.os.AsyncTask { 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 index 00000000..4131a213 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java @@ -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 . + */ + +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 { + private static final Pattern transactionStartPattern = Pattern.compile("([\\d.-]+)"); + private static final Pattern transactionDescriptionPattern = + Pattern.compile(" 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("

General Journal

")) + 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 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; + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/SaveTransactionTask.java b/app/src/main/java/net/ktnx/mobileledger/SaveTransactionTask.java index 6ebfc511..2e95a7d4 100644 --- a/app/src/main/java/net/ktnx/mobileledger/SaveTransactionTask.java +++ b/app/src/main/java/net/ktnx/mobileledger/SaveTransactionTask.java @@ -72,9 +72,9 @@ class SaveTransactionTask extends AsyncTask { Iterator 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 index 00000000..5217e5dd --- /dev/null +++ b/app/src/main/res/raw/sql_7.sql @@ -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 -- 2.39.5