]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/RetrieveTransactionsTask.java
machinery for retrieving transaction journal from hledger-web
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / RetrieveTransactionsTask.java
1 /*
2  * Copyright © 2018 Damyan Ivanov.
3  * This file is part of Mobile-Ledger.
4  * Mobile-Ledger is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * Mobile-Ledger is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger;
19
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.os.AsyncTask;
24
25 import java.io.BufferedReader;
26 import java.io.FileNotFoundException;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.InputStreamReader;
30 import java.lang.ref.WeakReference;
31 import java.net.HttpURLConnection;
32 import java.net.MalformedURLException;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35
36 class Params {
37     static final int DEFAULT_LIMIT = 100;
38     private SharedPreferences backendPref;
39     private String accountsRoot;
40     private int limit;
41
42     Params(SharedPreferences backendPref) {
43         this.backendPref = backendPref;
44         this.accountsRoot = null;
45         this.limit = DEFAULT_LIMIT;
46     }
47     Params(SharedPreferences backendPref, String accountsRoot) {
48         this(backendPref, accountsRoot, DEFAULT_LIMIT);
49     }
50     Params(SharedPreferences backendPref, String accountsRoot, int limit) {
51         this.backendPref = backendPref;
52         this.accountsRoot = accountsRoot;
53         this.limit = limit;
54     }
55     String getAccountsRoot() {
56         return accountsRoot;
57     }
58     SharedPreferences getBackendPref() {
59         return backendPref;
60     }
61     int getLimit() {
62         return limit;
63     }
64 }
65
66 class RetrieveTransactionsTask extends AsyncTask<Params, Integer, Void> {
67     private static final Pattern transactionStartPattern = Pattern.compile("<tr class=\"title\" "
68             + "id=\"transaction-(\\d+)\"><td class=\"date\"[^\\\"]*>([\\d.-]+)</td>");
69     private static final Pattern transactionDescriptionPattern =
70             Pattern.compile("<tr class=\"posting\" title=\"(\\S+)\\s(.+)");
71     private static final Pattern transactionDetailsPattern =
72             Pattern.compile("^\\s+" + "(\\S[\\S\\s]+\\S)\\s\\s+([-+]?\\d[\\d,.]*)");
73     protected WeakReference<Context> contextRef;
74     protected int error;
75     @Override
76     protected Void doInBackground(Params... params) {
77         try {
78             HttpURLConnection http =
79                     NetworkUtil.prepare_connection(params[0].getBackendPref(), "journal");
80             http.setAllowUserInteraction(false);
81             publishProgress(0);
82             Context ctx = contextRef.get();
83             if (ctx == null) return null;
84             try (MobileLedgerDatabase dbh = new MobileLedgerDatabase(ctx)) {
85                 try (SQLiteDatabase db = dbh.getWritableDatabase()) {
86                     try (InputStream resp = http.getInputStream()) {
87                         if (http.getResponseCode() != 200) throw new IOException(
88                                 String.format("HTTP error %d", http.getResponseCode()));
89                         db.beginTransaction();
90                         try {
91                             String root = params[0].getAccountsRoot();
92                             if (root == null) db.execSQL("DELETE FROM transaction_history;");
93                             else {
94                                 StringBuilder sql = new StringBuilder();
95                                 sql.append("DELETE FROM transaction_history ");
96                                 sql.append(
97                                         "where id in (select transactions.id from transactions ");
98                                 sql.append("join transaction_accounts ");
99                                 sql.append(
100                                         "on transactions.id=transaction_accounts.transaction_id ");
101                                 sql.append("where transaction_accounts.account_name like ?||'%'");
102                                 db.execSQL(sql.toString(), new String[]{root});
103                             }
104
105                             int state = ParserState.EXPECTING_JOURNAL;
106                             String line;
107                             BufferedReader buf =
108                                     new BufferedReader(new InputStreamReader(resp, "UTF-8"));
109
110                             int transactionCount = 0;
111                             String transactionId = null;
112                             LedgerTransaction transaction = null;
113                             while ((line = buf.readLine()) != null) {
114                                 switch (state) {
115                                     case ParserState.EXPECTING_JOURNAL: {
116                                         if (line.equals("<h2>General Journal</h2>"))
117                                             state = ParserState.EXPECTING_TRANSACTION;
118                                         continue;
119                                     }
120                                     case ParserState.EXPECTING_TRANSACTION: {
121                                         Matcher m = transactionStartPattern.matcher(line);
122                                         if (m.find()) {
123                                             transactionId = m.group(1);
124                                             state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
125                                         }
126                                     }
127                                     case ParserState.EXPECTING_TRANSACTION_DESCRIPTION: {
128                                         Matcher m = transactionDescriptionPattern.matcher(line);
129                                         if (m.find()) {
130                                             if (transactionId == null)
131                                                 throw new TransactionParserException(
132                                                         "Transaction Id is null while expecting description");
133
134                                             transaction =
135                                                     new LedgerTransaction(transactionId, m.group(1),
136                                                             m.group(2));
137                                             state = ParserState.EXPECTING_TRANSACTION_DETAILS;
138                                         }
139                                     }
140                                     case ParserState.EXPECTING_TRANSACTION_DETAILS: {
141                                         if (transaction == null)
142                                             throw new TransactionParserException(
143                                                     "Transaction is null while expecting details");
144                                         if (line.isEmpty()) {
145                                             // transaction data collected
146                                             transaction.insertInto(db);
147
148                                             state = ParserState.EXPECTING_TRANSACTION;
149                                             publishProgress(++transactionCount);
150                                         }
151                                         else {
152                                             Matcher m = transactionDetailsPattern.matcher(line);
153                                             if (m.find()) {
154                                                 String acc_name = m.group(1);
155                                                 String amount = m.group(2);
156                                                 amount = amount.replace(',', '.');
157                                                 transaction.add_item(
158                                                         new LedgerTransactionItem(acc_name,
159                                                                 Float.valueOf(amount)));
160                                             }
161                                             else throw new IllegalStateException(String.format(
162                                                     "Can't" + " parse transaction details"));
163                                         }
164                                     }
165                                     default:
166                                         throw new RuntimeException(
167                                                 String.format("Unknown " + "parser state %d",
168                                                         state));
169                                 }
170                             }
171                             db.setTransactionSuccessful();
172                         }
173                         finally {
174                             db.endTransaction();
175                         }
176                     }
177                 }
178             }
179         }
180         catch (MalformedURLException e) {
181             error = R.string.err_bad_backend_url;
182             e.printStackTrace();
183         }
184         catch (FileNotFoundException e) {
185             error = R.string.err_bad_auth;
186             e.printStackTrace();
187         }
188         catch (IOException e) {
189             error = R.string.err_net_io_error;
190             e.printStackTrace();
191         }
192         return null;
193     }
194     WeakReference<Context> getContextRef() {
195         return contextRef;
196     }
197
198     private class TransactionParserException extends IllegalStateException {
199         TransactionParserException(String message) {
200             super(message);
201         }
202     }
203
204     private class ParserState {
205         static final int EXPECTING_JOURNAL = 0;
206         static final int EXPECTING_TRANSACTION = 1;
207         static final int EXPECTING_TRANSACTION_DESCRIPTION = 2;
208         static final int EXPECTING_TRANSACTION_DETAILS = 3;
209     }
210 }