/* * Copyright (C) 2011 - 2013 Michi Gysel <michael.gysel@gmail.com> * * This file is part of the HSR Timetable. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ch.scythe.hsr.api; import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.StreamCorruptedException; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Date; import java.util.List; import net.iharder.base64.Base64; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicHttpResponse; import android.content.Context; import android.os.Build; import android.util.Log; import ch.scythe.hsr.api.ui.DataAssembler; import ch.scythe.hsr.api.ui.UiWeek; import ch.scythe.hsr.error.AccessDeniedException; import ch.scythe.hsr.error.ResponseParseException; import ch.scythe.hsr.error.ServerConnectionException; import ch.scythe.hsr.helper.AndroidHelper; import ch.scythe.hsr.helper.DateHelper; import ch.scythe.hsr.json.GsonParser; import ch.scythe.hsr.json.JsonTimetableWeek; public class TimeTableAPI { // _SOAP Webservice info private static final String URL = "https://stundenplanws.hsr.ch:4443/api/"; private static final String METHOD_GET_TIMETABLE = "Timetable/"; private static final String METHOD_GET_TIMEPERIOD = "Timeperiod/"; // _Cache details private static final String TIMETABLE_CACHE_SERIALIZED = "timetable_cache-v8.ser"; private static final String TIMETABLE_CACHE_TIMESTAMP = "timetable_timestamp-v8.txt"; // _Helper private final Context context; // _Logging details private static final String LOGGING_TAG = "TimeTableAPI"; // _API Headers private final String userAgent; private final String operatingSystem; public TimeTableAPI(Context context) { this.context = context; String appVersionName = AndroidHelper.getAppVersionName(context); userAgent = "HSRAndroidTimetable/" + appVersionName; operatingSystem = "Android/" + Build.VERSION.RELEASE; } /** @param forceRequest * Skips the caching mechanism and loads the data always from the web. * * @throws RequestException * If the timetable could not be successfully requested. * @throws ResponseParseException * If result contains not parsable data. * @throws ServerConnectionException * If the connection to the server if aborted * @throws AccessDeniedException * If the server returns a 401 (HTTP Error 401 Unauthorized Explained) */ public UiWeek retrieve(Date requestedDate, String login, String password, boolean forceRequest) throws RequestException, ResponseParseException, ServerConnectionException, AccessDeniedException { UiWeek result = null; // create cache if the cache is not present yet if (cacheUpdateRequired(forceRequest)) { if (forceRequest) { Log.i(LOGGING_TAG, "Started forced cache reloading."); } else { Log.i(LOGGING_TAG, "Started initial cache loading."); } String dateString = DateHelper.formatToTechnicalFormat(requestedDate); updateCache(dateString, null, login, password); } FileInputStream cachedRequest = null; try { // read the cached data Date cacheTimestamp = getCacheTimestamp(); // parse the timetable from the cache long before = System.currentTimeMillis(); cachedRequest = context.openFileInput(TIMETABLE_CACHE_SERIALIZED); ObjectInputStream dataStream = new ObjectInputStream(cachedRequest); result = (UiWeek) dataStream.readObject(); Log.i(LOGGING_TAG, "Deserialized data in " + (System.currentTimeMillis() - before) + "ms."); result.setLastUpdate(cacheTimestamp); } catch (FileNotFoundException e) { throw new RequestException(e); } catch (StreamCorruptedException e) { throw new ResponseParseException(e); } catch (IOException e) { throw new ResponseParseException(e); } catch (ClassNotFoundException e) { throw new ResponseParseException(e); } finally { safeCloseStream(cachedRequest); } return result; } public boolean validateCredentials(String login, String password) throws ServerConnectionException { boolean result = false; try { HttpGet get = createHttpGet(URL + METHOD_GET_TIMEPERIOD, login, password); HttpClient httpclient = new DefaultHttpClient(); BasicHttpResponse httpResponse = (BasicHttpResponse) httpclient.execute(get); int httpStatus = httpResponse.getStatusLine().getStatusCode(); result = HttpStatus.SC_OK == httpStatus; } catch (Exception e) { throw new ServerConnectionException(e); } return result; } public boolean retrieveRequiresBlockingCall(boolean forceRequest) { return true; } private boolean cacheUpdateRequired(boolean forceRequest) { return forceRequest || !cacheFilesExist(context.fileList(), TIMETABLE_CACHE_SERIALIZED, TIMETABLE_CACHE_TIMESTAMP); } private Date getCacheTimestamp() throws RequestException { Date cacheTimestamp = null; DataInputStream inputStream = null; try { inputStream = new DataInputStream(context.openFileInput(TIMETABLE_CACHE_TIMESTAMP)); cacheTimestamp = new Date(inputStream.readLong()); } catch (FileNotFoundException e) { throw new RequestException(e); } catch (IOException e) { throw new RequestException(e); } finally { safeCloseStream(inputStream); } return cacheTimestamp; } private void updateCache(String dateString, Date cacheTimestamp, String login, String password) throws RequestException, ServerConnectionException, ResponseParseException, AccessDeniedException { Log.i(LOGGING_TAG, "Starting to read data from the server."); long before = System.currentTimeMillis(); try { HttpGet get = createHttpGet(URL + METHOD_GET_TIMETABLE + login, login, password); HttpClient httpclient = new DefaultHttpClient(); BasicHttpResponse httpResponse = (BasicHttpResponse) httpclient.execute(get); InputStream jsonStream = null; int httpStatus = httpResponse.getStatusLine().getStatusCode(); if (httpStatus == HttpStatus.SC_OK) { jsonStream = httpResponse.getEntity().getContent(); } else if (httpStatus == HttpStatus.SC_UNAUTHORIZED) { throw new AccessDeniedException(); } else { throw new RequestException("Request not successful. \nHTTP Status: " + httpStatus); } Log.i(LOGGING_TAG, "Finished reading from server."); // convert JSON to Java objects JsonTimetableWeek serverData = new GsonParser().parse(jsonStream); UiWeek uiWeek = DataAssembler.convert(serverData); // open streams to cache the files DataOutputStream cacheTimestampOutputStream = new DataOutputStream(context.openFileOutput(TIMETABLE_CACHE_TIMESTAMP, Context.MODE_PRIVATE)); FileOutputStream xmlCacheOutputStream = context.openFileOutput(TIMETABLE_CACHE_SERIALIZED, Context.MODE_PRIVATE); // write data to streams ObjectOutputStream out = new ObjectOutputStream(xmlCacheOutputStream); out.writeObject(uiWeek); cacheTimestampOutputStream.writeLong(new Date().getTime()); safeCloseStream(xmlCacheOutputStream); safeCloseStream(cacheTimestampOutputStream); } catch (UnsupportedEncodingException e) { throw new RequestException(e); } catch (IllegalStateException e) { throw new RequestException(e); } catch (IOException e) { throw new ServerConnectionException(e); } Log.i(LOGGING_TAG, "Read and parsed data from the server in " + (System.currentTimeMillis() - before) + "ms."); } private HttpGet createHttpGet(String url, String login, String password) throws UnsupportedEncodingException { login = login.trim(); // prevent "Illegal character in path" Exception String basicAuth = "Basic " + Base64.encodeBytes((login + ":" + password).getBytes()); HttpGet get = new HttpGet(url); get.setHeader("Content-Type", "text/json;charset=UTF-8"); get.setHeader("User-Agent", userAgent); get.setHeader("Operating-System", operatingSystem); get.setHeader("Authorization", basicAuth); return get; } private void safeCloseStream(Closeable stream) { if (stream != null) { try { stream.close(); } catch (IOException e) { } } } private boolean cacheFilesExist(String[] existingFiles, String... filesToCheck) { boolean result = true; List<String> existingFilesList = Arrays.asList(existingFiles); for (String fileToCheck : filesToCheck) { if (!existingFilesList.contains(fileToCheck)) { result = false; break; } } return result; } }