DownloadManager.java 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. /*
  2. * DSBDirect
  3. * Copyright (C) 2019 Fynn Godau
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. *
  18. * This software is not affiliated with heinekingmedia GmbH, the
  19. * developer of the DSB platform.
  20. */
  21. package godau.fynn.dsbdirect.manager;
  22. import android.content.Context;
  23. import android.content.SharedPreferences;
  24. import android.graphics.Bitmap;
  25. import android.graphics.BitmapFactory;
  26. import android.net.ConnectivityManager;
  27. import android.net.NetworkInfo;
  28. import androidx.annotation.NonNull;
  29. import androidx.annotation.Nullable;
  30. import android.util.Base64;
  31. import android.util.Log;
  32. import godau.fynn.dsbdirect.table.*;
  33. import org.json.JSONArray;
  34. import org.json.JSONException;
  35. import org.json.JSONObject;
  36. import org.jsoup.Connection;
  37. import org.jsoup.Jsoup;
  38. import org.jsoup.nodes.Document;
  39. import org.jsoup.nodes.Element;
  40. import org.jsoup.select.Elements;
  41. import java.io.*;
  42. import java.net.HttpURLConnection;
  43. import java.net.URISyntaxException;
  44. import java.net.URL;
  45. import java.net.URLEncoder;
  46. import java.text.SimpleDateFormat;
  47. import java.util.ArrayList;
  48. import java.util.Arrays;
  49. import java.util.Date;
  50. import java.util.HashSet;
  51. import java.util.List;
  52. import java.util.zip.GZIPInputStream;
  53. import java.util.zip.GZIPOutputStream;
  54. import godau.fynn.dsbdirect.Login;
  55. import godau.fynn.dsbdirect.QueryMetadata;
  56. import godau.fynn.dsbdirect.R;
  57. import godau.fynn.dsbdirect.Utility;
  58. import godau.fynn.dsbdirect.table.reader.Reader;
  59. public class DownloadManager {
  60. private Context mContext;
  61. public DownloadManager(Context context) {
  62. mContext = context;
  63. }
  64. /**
  65. * Make a GET request (synchronously)
  66. *
  67. * @param url URL to be requested
  68. * @param body Request body (JSON String)
  69. * @param requestMethod Usually either GET or POST
  70. * @return Response
  71. * @throws IOException If networking error or other IO exception
  72. */
  73. private @NonNull
  74. InputStream request(String url, @Nullable String body, String requestMethod) throws IOException {
  75. if (!isNetworkAvailable()) throw new IOException();
  76. // Encode the url correctly, as the file name part can contain Umlaute or other weird things
  77. String[] urlParts =
  78. url.split("/(?!.*/)"); // Matches only the last '/' character (using a negative lookahead)
  79. url = urlParts[0] + "/" + URLEncoder.encode(
  80. urlParts[1] // This is the part that has to be encoded correctly
  81. .replaceAll(
  82. "%20", " " /* Spaces are already encoded as "%20". Let's decode them quickly so we won't have
  83. * %20 encoded as something like "%2520"
  84. */
  85. ), "ISO-8859-1" // UTF-8 won't work here
  86. )
  87. .replaceAll(
  88. "\\+", "%20" /* Spaces are encoded again, but they are now '+' chars. That's unfortunately not
  89. * correct. Let's replace them with "%20"s.
  90. */
  91. );
  92. URL connectwat = new URL(url);
  93. HttpURLConnection urlConnection = (HttpURLConnection) connectwat.openConnection();
  94. urlConnection.setRequestMethod(requestMethod);
  95. // Add DNT header, as if it does anything
  96. urlConnection.addRequestProperty("DNT", "1");
  97. if (body != null) {
  98. // Get headers from sharedPreferences so they can be set through news
  99. HashSet queryHeaders = (HashSet) new Utility(mContext).getSharedPreferences()
  100. .getStringSet("queryHeaders", new HashSet<>(Arrays.asList(
  101. "Referer: https://www.dsbmobile.de/",
  102. "Content-Type: application/json;charset=utf-8"
  103. )));
  104. // Add each header to query
  105. for (Object header : queryHeaders) {
  106. String queryHeader = (String) header;
  107. // Split header into two parts
  108. String[] queryHeaderParts = queryHeader.split(": ");
  109. // Check whether header really is two parts
  110. if (queryHeaderParts.length != 2) {
  111. Log.e("DOWNLOADHEADER", "invalid header: " + queryHeader);
  112. continue;
  113. }
  114. // Add header to request
  115. urlConnection.setRequestProperty(queryHeaderParts[0], queryHeaderParts[1]);
  116. Log.d("DOWNLOADHEADER", queryHeader);
  117. }
  118. OutputStream outputStream = urlConnection.getOutputStream();
  119. OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
  120. outputStreamWriter.write(body);
  121. outputStreamWriter.flush();
  122. outputStreamWriter.close();
  123. outputStream.close();
  124. }
  125. urlConnection.connect();
  126. return new BufferedInputStream(urlConnection.getInputStream());
  127. }
  128. /**
  129. * Downloads a list of all contents currently available from DSB (synchronously)
  130. *
  131. * @param login Login to log in with
  132. * @return A ContentInformation object with all contents in DSB
  133. * @throws LoginFailureException If the credentials or the request in general are incorrect
  134. * @throws UnexpectedResponseException If response is invalid JSON
  135. * @throws IOException If request fails in general (networking error?)
  136. */
  137. public ContentInformation downloadContentInformation(Login login) throws IOException {
  138. JSONArray contentArray = downloadContentJSONArray(login);
  139. Table[] tables = null;
  140. List<Notice> notices = null;
  141. List<NewsItem> news = null;
  142. try {
  143. for (int i = 0; i < contentArray.length(); i++) {
  144. JSONObject contentObject = contentArray.getJSONObject(i);
  145. String contentName = contentObject.getString("Title");
  146. JSONArray childs = contentObject.getJSONObject("Root").getJSONArray("Childs");
  147. // It has been observed that News are before Pläne if they exist
  148. switch (contentName) {
  149. case "Pläne":
  150. tables = Reader.readTableList(childs);
  151. break;
  152. case "News":
  153. news = Reader.readNewsList(childs);
  154. break;
  155. case "Aushänge":
  156. notices = Reader.readNoticeList(childs);
  157. }
  158. }
  159. } catch (JSONException e) {
  160. // Response is invalid, throw further
  161. throw new UnexpectedResponseException(e.getCause());
  162. }
  163. return new ContentInformation(tables, notices, news);
  164. }
  165. /**
  166. * Downloads and returns the Childs JSON array of Inhalte from DSB (synchronously)
  167. *
  168. * @param login Login to log in with
  169. * @return JSON array containing the Childs of Inhalte of server's response
  170. * @throws LoginFailureException If the credentials or the request in general are incorrect
  171. * @throws UnexpectedResponseException If response is invalid JSON
  172. * @throws IOException If request fails in general (networking error?)
  173. */
  174. private JSONArray downloadContentJSONArray(Login login) throws IOException {
  175. Log.d("DOWNLOAD", "downloading data");
  176. if (!login.isNonZeroLength()) {
  177. // Empty credentials are not valid
  178. throw new LoginFailureException();
  179. }
  180. // Make request body
  181. JSONObject bodyObject;
  182. SharedPreferences sharedPreferences = new Utility(mContext).getSharedPreferences();
  183. try {
  184. // Query body base json might be overwritten by news, otherwise use hardcoded value
  185. String queryBodyBaseJson = sharedPreferences
  186. .getString("queryBodyBaseJson", mContext.getString(R.string.query_body_base_json));
  187. Log.d("DOWNLOADBASEJSON", queryBodyBaseJson);
  188. bodyObject = new JSONObject(queryBodyBaseJson);
  189. login.put(bodyObject);
  190. // Add things configurable through news
  191. boolean sendAppId = sharedPreferences.getBoolean("querySendAppId", true);
  192. boolean sendAndroidVersion = sharedPreferences.getBoolean("querySendAndroidVersion", true);
  193. boolean sendDeviceModel = sharedPreferences.getBoolean("querySendDeviceModel", true);
  194. boolean sendLanguage = sharedPreferences.getBoolean("querySendLanguage", true);
  195. boolean sendDate = sharedPreferences.getBoolean("querySendDate", false);
  196. boolean sendLastDate = sharedPreferences.getBoolean("querySendLastDate", false);
  197. // Generate AppId
  198. if (sendAppId) {
  199. bodyObject.put("AppId", QueryMetadata.getAppId());
  200. }
  201. // Attach random android version
  202. if (sendAndroidVersion) {
  203. bodyObject.put("OsVersion", QueryMetadata.getAndroidVersion());
  204. }
  205. // Attach random device name
  206. if (sendDeviceModel) {
  207. bodyObject.put("Device", QueryMetadata.getDeviceModel());
  208. }
  209. // Attach some language
  210. if (sendLanguage) {
  211. bodyObject.put("Language", QueryMetadata.getLanguage());
  212. }
  213. // Send date
  214. if (sendDate) {
  215. // Date should look like this: 2019-10-04T14:21:3728600
  216. SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss:SSSS000");
  217. String date = dateFormat.format(new Date());
  218. Log.d("DOWNLOADDATE", date);
  219. bodyObject.put("Date", date);
  220. // Send last date
  221. if (sendLastDate) {
  222. bodyObject.put("LastUpdate", sharedPreferences.getString("queryLastDate", ""));
  223. sharedPreferences.edit()
  224. .putString("queryLastDate", date)
  225. .apply();
  226. }
  227. }
  228. } catch (JSONException e) {
  229. e.printStackTrace();
  230. // Shouldn't happen! Throw further as IOException since we don't really know what happened anyway
  231. throw new IOException(e.getCause());
  232. }
  233. // Request url
  234. String url = getEndpoint(sharedPreferences.getInt("queryEndpoint", 0));
  235. Log.d("DOWNLOADENDPOINT", url);
  236. // Request
  237. String response = string(
  238. request(url, obfuscateQuery(bodyObject), "POST"),
  239. "UTF-8"
  240. );
  241. // If request is very invalid, there won't be any json in the response, only some plain text…
  242. if (response.equals("Unzulässige Anforderung")) {
  243. Log.d("DOWNLOAD", "request failed: " + bodyObject.toString() + " obfuscated to " + obfuscateQuery(bodyObject));
  244. throw new UnexpectedResponseException();
  245. }
  246. try {
  247. JSONObject responseBody = deobfuscateResponse(response);
  248. // Check result code
  249. int resultcode = responseBody.getInt("Resultcode");
  250. switch (resultcode) {
  251. case 0:
  252. // All is good, continue
  253. break;
  254. case 1:
  255. // Invalid credentials ("ResultStatusInfo": "Login fehlgeschlagen")
  256. throw new LoginFailureException();
  257. default:
  258. Log.d("DOWNLOAD", "unexpected Resultcode " + resultcode + ": " + response);
  259. throw new UnexpectedResponseException();
  260. }
  261. JSONArray resultMenuItems = responseBody.getJSONArray("ResultMenuItems");
  262. resultMenu:
  263. for (int i = 0; i < resultMenuItems.length(); i++) {
  264. JSONObject resultMenuItem = resultMenuItems.getJSONObject(i);
  265. String resultMenuItemTitle = resultMenuItem.getString("Title");
  266. // Just to be sure that we select Inhalte, in practice it has only been observed to be the first one
  267. if (resultMenuItemTitle.equals("Inhalte")) {
  268. return resultMenuItem.getJSONArray("Childs");
  269. }
  270. }
  271. // No Inhalte…?
  272. throw new UnexpectedResponseException("No Inhalte found");
  273. } catch (JSONException | EOFException e) {
  274. e.printStackTrace();
  275. // Response is invalid, throw further
  276. throw new UnexpectedResponseException(e.getCause());
  277. }
  278. }
  279. /**
  280. * Return the corresponding url for the endpoint id.
  281. *
  282. * @param id 0 (mobile) / 1 (web) / 2 (ihkmobile) / 3 (appihkbb)
  283. */
  284. private String getEndpoint(int id) throws IOException {
  285. switch (id) {
  286. case 0:
  287. default:
  288. return "https://app.dsbcontrol.de/JsonHandler.ashx/GetData";
  289. case 1:
  290. String webConfiguration = string(
  291. request("https://www.dsbmobile.de/scripts/configuration.js", null, "GET"),
  292. "UTF-8"
  293. );
  294. return "https://www.dsbmobile.de/" + webConfiguration.split("'")[3]; //bad solution, web endpoint obfuscated -> method outdated
  295. case 2:
  296. return "https://ihkmobile.dsbcontrol.de/new/JsonHandlerWeb.ashx/GetData";
  297. case 3:
  298. return "https://appihkbb.dsbcontrol.de/new/JsonHandlerWeb.ashx/GetData";
  299. }
  300. }
  301. /**
  302. * Downloads an html table file (synchronously)
  303. *
  304. * @param table The table which is to be downloaded
  305. * @param fileManager A file manager to be used to save the file
  306. * @return The downloaded html String
  307. * @throws IOException In case of networking error
  308. */
  309. public String downloadHtmlTable(final Table table, final FileManager fileManager) throws IOException { // TODO
  310. Log.d("DOWNLOAD", "downloading html file at " + table.getUri());
  311. // Request
  312. String html = string(request(table.getUri(), null, "GET"), "ISO-8859-1");
  313. // If these characters appear, the wrong encoding has been used
  314. if (html.contains("")) {
  315. try {
  316. html = new String(html.getBytes("ISO-8859-1"), "UTF-8");
  317. } catch (UnsupportedEncodingException e) {
  318. // So be it!
  319. e.printStackTrace();
  320. }
  321. }
  322. // Save file
  323. fileManager.saveFile(table, html);
  324. return html;
  325. }
  326. /**
  327. * Downloads a bitmap table file (synchronously)
  328. *
  329. * @param table The table which is to be downloaded
  330. * @param fileManager A file manager to be used to save the file
  331. * @return The bitmap
  332. * @throws IOException In case of networking error
  333. */
  334. public Bitmap downloadImageTable(final Table table, final FileManager fileManager) throws IOException { // TODO
  335. // We're doing bitmaps.
  336. Log.d("DOWNLOAD", "downloading image file at " + table.getUri());
  337. // Request bitmap
  338. InputStream inputStream = request(table.getUri(), null, "GET");
  339. // Create bitmap
  340. Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
  341. // Save bitmap
  342. fileManager.saveFile(table, bitmap);
  343. // Return bitmap
  344. return bitmap;
  345. }
  346. /**
  347. * Obfuscate query. The DSB server requires this.
  348. * <br/><br/>
  349. * Queries are "compressed" using gzip (saving less than half a kilobyte) and then encoded using base64.
  350. * The result of that is then again hidden inside some more JSON.
  351. *
  352. * @param query The JSON query you want to make
  353. * @return The body you will have to send to the server to execute the query
  354. */
  355. private String obfuscateQuery(JSONObject query) throws IOException {
  356. String queryString = query.toString();
  357. // Thanks, https://stackoverflow.com/a/6718707
  358. ByteArrayOutputStream outputStream = new ByteArrayOutputStream(queryString.length());
  359. GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
  360. gzip.write(queryString.getBytes());
  361. gzip.close();
  362. byte[] gzipped = outputStream.toByteArray();
  363. outputStream.close();
  364. String encoded = Base64.encodeToString(gzipped, Base64.NO_WRAP); // Line wraps are useless here!
  365. return mContext.getString(R.string.query_body_outer_json, encoded);
  366. }
  367. /**
  368. * Deobfuscate response. The DSB server gives obfuscated responses.
  369. * <br/><br/>
  370. * Just the reverse of {@link #obfuscateQuery(JSONObject)}, except that "some more JSON" is different for responses
  371. * compared to queries.
  372. *
  373. * @param response The response the server gave you
  374. * @return The JSON hidden inside the response
  375. */
  376. private JSONObject deobfuscateResponse(String response) throws JSONException, IOException {
  377. JSONObject responseObject = new JSONObject(response);
  378. String encoded = responseObject.getString("d");
  379. byte[] gzipped = Base64.decode(encoded, Base64.DEFAULT);
  380. // Who knows how this works… thanks again, https://stackoverflow.com/a/6718707
  381. final int BUFFER_SIZE = 32;
  382. ByteArrayInputStream is = new ByteArrayInputStream(gzipped);
  383. GZIPInputStream gis = new GZIPInputStream(is, BUFFER_SIZE);
  384. StringBuilder stringBuilder = new StringBuilder();
  385. byte[] data = new byte[BUFFER_SIZE];
  386. int bytesRead;
  387. while ((bytesRead = gis.read(data)) != -1) {
  388. stringBuilder.append(new String(data, 0, bytesRead));
  389. }
  390. gis.close();
  391. is.close();
  392. return new JSONObject(stringBuilder.toString());
  393. }
  394. public JSONObject downloadUpdateCheck() throws IOException {
  395. Log.d("DOWNLOAD", "downloading update check");
  396. try {
  397. return new JSONObject(string(request(mContext.getString(R.string.uri_versioncheck), null, "GET"), "UTF-8"));
  398. } catch (JSONException e) {
  399. throw new UnexpectedResponseException(e.getCause());
  400. }
  401. }
  402. public JSONObject downloadNews() throws IOException {
  403. Log.d("DOWNLOAD", "downloading news");
  404. try {
  405. return new JSONObject(string(request(mContext.getString(R.string.uri_news), null, "GET"), "UTF-8"));
  406. } catch (JSONException e) {
  407. throw new UnexpectedResponseException(e.getCause());
  408. }
  409. }
  410. // Thanks, https://stackoverflow.com/a/35446009
  411. private String string(InputStream in, String charsetName) throws IOException {
  412. ByteArrayOutputStream result = new ByteArrayOutputStream();
  413. byte[] buffer = new byte[1024];
  414. int length;
  415. while ((length = in.read(buffer)) != -1) {
  416. result.write(buffer, 0, length);
  417. }
  418. return result.toString(charsetName);
  419. }
  420. /**
  421. * Uploads a url to the server at https://dsb.bixilon.de to ask the developer to develop a parser for it.
  422. * <br/><br/>The server code is available at <a href="https://notabug.org/fynngodau/dsbdirect-filedump/src/master/requestParser.php">fynngodau/dsbdirect-filedump</a>.
  423. *
  424. * @param url The url to upload. Must be at <a href="https://app.dsbcontrol.de">https://app.dsbcontrol.de</a>
  425. * @return whether the server returned 200 Success
  426. * @throws IOException in case of a network error
  427. */
  428. public boolean uploadParserRequest(String url) throws IOException {
  429. // request(…) returns an InputStream, not the response code as it is pretty much always 200, so we can't use it here
  430. if (!isNetworkAvailable()) {
  431. throw new IOException();
  432. }
  433. URL connectwat = new URL(mContext.getString(R.string.uri_requestparser));
  434. HttpURLConnection urlConnection = (HttpURLConnection) connectwat.openConnection();
  435. urlConnection.setRequestMethod("POST");
  436. OutputStream outputStream = urlConnection.getOutputStream();
  437. OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
  438. outputStreamWriter.write("url=" + url);
  439. outputStreamWriter.flush();
  440. outputStreamWriter.close();
  441. outputStream.close();
  442. urlConnection.connect();
  443. if (urlConnection.getResponseCode() == 200) return true;
  444. else return false;
  445. }
  446. private boolean isNetworkAvailable() {
  447. ConnectivityManager connectivityManager
  448. = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
  449. NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
  450. return activeNetworkInfo != null && activeNetworkInfo.isConnected();
  451. }
  452. /**
  453. * Download all shortcodes from eltern-portal.org (synchronously)
  454. *
  455. * @param url URL to be requested
  456. * @param email Login Username (email)
  457. * @param password Password for login
  458. * @return Shortcode null on error, array on success
  459. * @throws IOException If networking error or other IO exception
  460. * @throws IllegalArgumentException If the provided url is incorrect
  461. * @throws LoginFailureException If the credentials were not correct
  462. */
  463. public Shortcode[] downloadShortcodesFromElternportal(String url, String email, String password)
  464. throws IOException, IllegalArgumentException, LoginFailureException {
  465. //is network available?
  466. if (!isNetworkAvailable()) throw new IOException();
  467. //attach an / if needed
  468. if (!url.endsWith("/"))
  469. url = url + "/";
  470. //check url
  471. if (!url.endsWith(".eltern-portal.org/")) {
  472. //url invalid
  473. throw new IllegalArgumentException();
  474. }
  475. if (url.startsWith("http://")) {
  476. url = url.replace("http://", "https://"); //force https
  477. }
  478. if (!url.toLowerCase().matches("^\\w+://.*")) {
  479. url = "https://" + url;
  480. }
  481. //check if url is parsable
  482. try {
  483. URL u = new URL(url);
  484. u.toURI();
  485. } catch (URISyntaxException e) {
  486. //nope. not valid
  487. e.printStackTrace();
  488. throw new IllegalArgumentException(e);
  489. }
  490. //obtain session key
  491. Connection.Response res = null;
  492. res = Jsoup.connect(url)
  493. .execute();
  494. //login with obtained session key
  495. res = Jsoup.connect(url + "includes/project/auth/login.php")
  496. .data("username", email, "password", password)
  497. .method(Connection.Method.POST)
  498. .cookies(res.cookies())
  499. .followRedirects(false)
  500. .execute();
  501. if (res.body().contains("Fehler bei der Anmeldung") || res.header("Location").contains(".eltern-portal.org/login?errno=1&username=")) {
  502. //fail: wrong session key or username/password wrong
  503. throw new LoginFailureException();
  504. }
  505. //get html of school information
  506. Document doc = Jsoup.connect(url + "service/schulinformationen")
  507. .cookies(res.cookies())
  508. .get();
  509. Elements e = doc.getElementById("asam_content").select("div[class=row m_bot]");
  510. boolean getthem = false; //there are not only shotcodes also school infos. Wait until shortcodes appear
  511. List<Shortcode> shortcodes = new ArrayList<Shortcode>();
  512. for (Element s : e) {
  513. String raw = s.select("div[class=col-md-4],div[class=col-md-6]").text();
  514. //empty line. skipping
  515. if (raw.equals(""))
  516. continue;
  517. if (raw.contains("Homepage")) { //Homepage is the last school info. Follows by shortcodes
  518. getthem = true;
  519. continue;
  520. }
  521. if (!getthem)
  522. continue;
  523. String split[] = raw.split(" ", 3); //short short n a m e
  524. shortcodes.add(new Shortcode(split[0], split[2]));
  525. }
  526. Shortcode array[] = new Shortcode[shortcodes.size()];
  527. array = shortcodes.toArray(array);
  528. return array;
  529. }
  530. /**
  531. * Holds information about all the content that is currently offered at the DSB. Notices
  532. * and news are treated as the same.
  533. */
  534. public static class ContentInformation implements Serializable {
  535. private Table[] tables;
  536. private ArrayList<NoticeBoardItem> notices;
  537. public ContentInformation(Table[] tables, List<Notice> notices, List<NewsItem> news) {
  538. this.tables = tables == null ? new Table[0] : tables;
  539. this.notices = new ArrayList<>();
  540. if (notices != null) this.notices.addAll(notices);
  541. if (news != null) this.notices.addAll(news);
  542. }
  543. public Table[] getTables() {
  544. return tables;
  545. }
  546. public ArrayList<NoticeBoardItem> getNotices() {
  547. return notices;
  548. }
  549. /**
  550. * @return Whether at least one notice board item is available.
  551. */
  552. public boolean hasNotices() {
  553. return notices.size() > 0;
  554. }
  555. }
  556. static public class UnexpectedResponseException extends IOException {
  557. public UnexpectedResponseException() {
  558. super();
  559. }
  560. public UnexpectedResponseException(Throwable cause) {
  561. super(cause);
  562. }
  563. public UnexpectedResponseException(String message) {
  564. super(message);
  565. }
  566. }
  567. /**
  568. * Possible causes: credentials incorrect, server rejected login due to an invalid request
  569. */
  570. static public class LoginFailureException extends UnexpectedResponseException {
  571. }
  572. static public class NoContentException extends UnexpectedResponseException {
  573. }
  574. }