]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/async/SendTransactionTask.java
whitespace
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / async / SendTransactionTask.java
1 /*
2  * Copyright © 2019 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe 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  * MoLe 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 MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.async;
19
20 import android.os.AsyncTask;
21 import android.util.Log;
22
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.databind.ObjectWriter;
25
26 import net.ktnx.mobileledger.json.ParsedLedgerTransaction;
27 import net.ktnx.mobileledger.model.LedgerTransaction;
28 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
29 import net.ktnx.mobileledger.model.MobileLedgerProfile;
30 import net.ktnx.mobileledger.utils.Globals;
31 import net.ktnx.mobileledger.utils.NetworkUtil;
32 import net.ktnx.mobileledger.utils.UrlEncodedFormData;
33
34 import java.io.BufferedReader;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.io.OutputStream;
39 import java.net.HttpURLConnection;
40 import java.nio.charset.StandardCharsets;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.Map;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
46
47 import static android.os.SystemClock.sleep;
48 import static net.ktnx.mobileledger.utils.Logger.debug;
49
50 public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void> {
51     private final TaskCallback taskCallback;
52     protected String error;
53     private String token;
54     private String session;
55     private LedgerTransaction ltr;
56     private MobileLedgerProfile mProfile;
57
58     public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile) {
59         taskCallback = callback;
60         mProfile = profile;
61     }
62     private boolean sendOK() throws IOException {
63         HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
64         http.setRequestMethod("PUT");
65         http.setRequestProperty("Content-Type", "application/json");
66         http.setRequestProperty("Accept", "*/*");
67
68         ParsedLedgerTransaction jsonTransaction;
69         jsonTransaction = ltr.toParsedLedgerTransaction();
70         ObjectMapper mapper = new ObjectMapper();
71         ObjectWriter writer = mapper.writerFor(ParsedLedgerTransaction.class);
72         String body = writer.writeValueAsString(jsonTransaction);
73
74         byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
75         http.setDoOutput(true);
76         http.setDoInput(true);
77         http.addRequestProperty("Content-Length", String.valueOf(bodyBytes.length));
78
79         debug("network", "request header: " + http.getRequestProperties()
80                                                   .toString());
81
82         try (OutputStream req = http.getOutputStream()) {
83             debug("network", "Request body: " + body);
84             req.write(bodyBytes);
85
86             final int responseCode = http.getResponseCode();
87             debug("network",
88                     String.format("Response: %d %s", responseCode, http.getResponseMessage()));
89
90             try (InputStream resp = http.getErrorStream()) {
91
92                 switch (responseCode) {
93                     case 200:
94                     case 201:
95                         break;
96                     case 405:
97                         return false; // will cause a retry with the legacy method
98                     default:
99                         BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
100                         String line = reader.readLine();
101                         debug("network", "Response content: " + line);
102                         throw new IOException(
103                                 String.format("Error response code %d", responseCode));
104                 }
105             }
106         }
107
108         return true;
109     }
110     private boolean legacySendOK() throws IOException {
111         HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
112         http.setRequestMethod("POST");
113         http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
114         http.setRequestProperty("Accept", "*/*");
115         if ((session != null) && !session.isEmpty()) {
116             http.setRequestProperty("Cookie", String.format("_SESSION=%s", session));
117         }
118         http.setDoOutput(true);
119         http.setDoInput(true);
120
121         UrlEncodedFormData params = new UrlEncodedFormData();
122         params.addPair("_formid", "identify-add");
123         if (token != null)
124             params.addPair("_token", token);
125         params.addPair("date", Globals.formatLedgerDate(ltr.getDate()));
126         params.addPair("description", ltr.getDescription());
127         for (LedgerTransactionAccount acc : ltr.getAccounts()) {
128             params.addPair("account", acc.getAccountName());
129             if (acc.isAmountSet())
130                 params.addPair("amount", String.format(Locale.US, "%1.2f", acc.getAmount()));
131             else
132                 params.addPair("amount", "");
133         }
134
135         String body = params.toString();
136         http.addRequestProperty("Content-Length", String.valueOf(body.length()));
137
138         debug("network", "request header: " + http.getRequestProperties()
139                                                   .toString());
140
141         try (OutputStream req = http.getOutputStream()) {
142             debug("network", "Request body: " + body);
143             req.write(body.getBytes(StandardCharsets.US_ASCII));
144
145             try (InputStream resp = http.getInputStream()) {
146                 debug("update_accounts", String.valueOf(http.getResponseCode()));
147                 if (http.getResponseCode() == 303) {
148                     // everything is fine
149                     return true;
150                 }
151                 else if (http.getResponseCode() == 200) {
152                     // get the new cookie
153                     {
154                         Pattern reSessionCookie = Pattern.compile("_SESSION=([^;]+);.*");
155
156                         Map<String, List<String>> header = http.getHeaderFields();
157                         List<String> cookieHeader = header.get("Set-Cookie");
158                         if (cookieHeader != null) {
159                             String cookie = cookieHeader.get(0);
160                             Matcher m = reSessionCookie.matcher(cookie);
161                             if (m.matches()) {
162                                 session = m.group(1);
163                                 debug("network", "new session is " + session);
164                             }
165                             else {
166                                 debug("network", "set-cookie: " + cookie);
167                                 Log.w("network",
168                                         "Response Set-Cookie headers is not a _SESSION one");
169                             }
170                         }
171                         else {
172                             Log.w("network", "Response has no Set-Cookie header");
173                         }
174                     }
175                     // the token needs to be updated
176                     BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
177                     Pattern re = Pattern.compile(
178                             "<input type=\"hidden\" name=\"_token\" value=\"([^\"]+)\">");
179                     String line;
180                     while ((line = reader.readLine()) != null) {
181                         //debug("dump", line);
182                         Matcher m = re.matcher(line);
183                         if (m.matches()) {
184                             token = m.group(1);
185                             debug("save-transaction", line);
186                             debug("save-transaction", "Token=" + token);
187                             return false;       // retry
188                         }
189                     }
190                     throw new IOException("Can't find _token string");
191                 }
192                 else {
193                     throw new IOException(
194                             String.format("Error response code %d", http.getResponseCode()));
195                 }
196             }
197         }
198     }
199
200     @Override
201     protected Void doInBackground(LedgerTransaction... ledgerTransactions) {
202         error = null;
203         try {
204             ltr = ledgerTransactions[0];
205
206             if (!sendOK()) {
207                 int tried = 0;
208                 while (!legacySendOK()) {
209                     tried++;
210                     if (tried >= 2)
211                         throw new IOException(String.format("aborting after %d tries", tried));
212                     sleep(100);
213                 }
214             }
215         }
216         catch (Exception e) {
217             e.printStackTrace();
218             error = e.getMessage();
219         }
220
221         return null;
222     }
223
224     @Override
225     protected void onPostExecute(Void aVoid) {
226         super.onPostExecute(aVoid);
227         taskCallback.done(error);
228     }
229 }