/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * 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 nya.miku.wishmaster.http.streamer; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.HashMap; import nya.miku.wishmaster.api.interfaces.CancellableTask; import nya.miku.wishmaster.api.interfaces.ProgressListener; import nya.miku.wishmaster.common.IOUtils; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.http.client.ExtendedHttpClient; import nya.miku.wishmaster.lib.org_json.JSONArray; import nya.miku.wishmaster.lib.org_json.JSONException; import nya.miku.wishmaster.lib.org_json.JSONObject; import nya.miku.wishmaster.lib.org_json.JSONTokener; import cz.msebera.android.httpclient.Header; import cz.msebera.android.httpclient.HttpEntity; import cz.msebera.android.httpclient.HttpHeaders; import cz.msebera.android.httpclient.HttpResponse; import cz.msebera.android.httpclient.StatusLine; import cz.msebera.android.httpclient.client.HttpClient; import cz.msebera.android.httpclient.client.config.RequestConfig; import cz.msebera.android.httpclient.client.methods.HttpUriRequest; import cz.msebera.android.httpclient.client.methods.RequestBuilder; import cz.msebera.android.httpclient.message.BasicHeader; /** * Выполнение HTTP запросов. * @author miku-nyan * */ public class HttpStreamer { private static final String TAG = "HttpStreamer"; private HttpStreamer() {} private static HttpStreamer instance = null; /** * Вызывается в {@link android.app.Application#onCreate()} */ public static void initInstance() { if (instance == null) instance = new HttpStreamer(); } public static HttpStreamer getInstance() { if (instance == null) { //should never happens Logger.e(TAG, "HttpStreamer is not initialized"); initInstance(); } return instance; } /** таблица с временами If-Modified-Since */ private final HashMap<String, String> ifModifiedMap = new HashMap<String, String>(); /** * Удалить url из таблицы времён изменения If-Modified-Since * @param url * @return значение удалённого элемента, или NULL, если такой элемент не был найден */ public String removeFromModifiedMap(String url) { String value = null; synchronized (ifModifiedMap) { value = ifModifiedMap.remove(url); } return value; } /** * HTTP запрос по адресу. После завершения работы с запросом, необходимо выполнить метод release() модели ответа! * Если по данному адресу предполагаются запросы с заголовком If-Modified-Since, в случае ошибки при дальнейшем чтении из потока * необходимо очистить соответствующию запись в таблице времён Modified ({@link #removeFromModifiedMap(String)})! * @param url адрес страницы * @param requestModel модель запроса (может принимать null, по умолчанию GET без проверки If-Modified) * @param httpClient HTTP клиент, исполняющий запрос * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @return модель содержит поток и другие данные HTTP ответа * @throws HttpRequestException исключение, если не удалось выполнить запрос */ public HttpResponseModel getFromUrl(String url, HttpRequestModel requestModel, HttpClient httpClient, ProgressListener listener, CancellableTask task) throws HttpRequestException { if (requestModel == null) requestModel = HttpRequestModel.DEFAULT_GET; //подготавливаем Request HttpUriRequest request = null; try { RequestConfig requestConfigBuilder = ExtendedHttpClient. getDefaultRequestConfigBuilder(requestModel.timeoutValue). setRedirectsEnabled(!requestModel.noRedirect). build(); RequestBuilder requestBuilder = null; switch (requestModel.method) { case HttpRequestModel.METHOD_GET: requestBuilder = RequestBuilder.get().setUri(url); break; case HttpRequestModel.METHOD_POST: requestBuilder = RequestBuilder.post().setUri(url).setEntity(requestModel.postEntity); break; default: throw new IllegalArgumentException("Incorrect type of HTTP Request"); } if (requestModel.customHeaders != null) { for (Header header : requestModel.customHeaders) { requestBuilder.addHeader(header); } } if (requestModel.checkIfModified && requestModel.method == HttpRequestModel.METHOD_GET) { synchronized (ifModifiedMap) { if (ifModifiedMap.containsKey(url)) { requestBuilder.addHeader(new BasicHeader(HttpHeaders.IF_MODIFIED_SINCE, ifModifiedMap.get(url))); } } } request = requestBuilder.setConfig(requestConfigBuilder).build(); } catch (Exception e) { Logger.e(TAG, e); HttpResponseModel.release(request, null); throw new IllegalArgumentException(e); } //делаем запрос HttpResponseModel responseModel = new HttpResponseModel(); HttpResponse response = null; try { IOException responseException = null; for (int i=0; i<5; ++i) { try { if (task != null && task.isCancelled()) throw new InterruptedException(); response = httpClient.execute(request); responseException = null; break; } catch (IOException e) { Logger.e(TAG, e); responseException = e; if (e.getMessage() == null) break; String message = e.getMessage(); if (message.indexOf("Connection reset by peer") != -1 || message.indexOf("I/O error during system call, Broken pipe") != -1 || (message.indexOf("Write error: ssl") != -1 && message.indexOf("I/O error during system call") != -1)) { continue; } else { break; } } } if (responseException != null) { throw responseException; } if (task != null && task.isCancelled()) throw new InterruptedException(); //обработка кода состояния HTTP StatusLine status = response.getStatusLine(); responseModel.statusCode = status.getStatusCode(); responseModel.statusReason = status.getReasonPhrase(); //обрабока полученных заголовков (headers) String lastModifiedValue = null; if (responseModel.statusCode == 200) { Header header = response.getFirstHeader(HttpHeaders.LAST_MODIFIED); if (header != null) lastModifiedValue = header.getValue(); } Header header = response.getFirstHeader(HttpHeaders.LOCATION); if (header != null) responseModel.locationHeader = header.getValue(); responseModel.headers = response.getAllHeaders(); //поток HttpEntity responseEntity = response.getEntity(); if (responseEntity != null) { responseModel.contentLength = responseEntity.getContentLength(); if (listener != null) listener.setMaxValue(responseModel.contentLength); InputStream stream = responseEntity.getContent(); responseModel.stream = IOUtils.modifyInputStream(stream, listener, task); } responseModel.request = request; responseModel.response = response; if (lastModifiedValue != null) { synchronized (ifModifiedMap) { ifModifiedMap.put(url, lastModifiedValue); } } } catch (Exception e) { Logger.e(TAG, e); HttpResponseModel.release(request, response); throw new HttpRequestException(e); } return responseModel; } /** * HTTP запрос по адресу, получить массив байт * @param url адрес страницы * @param requestModel модель запроса (может принимать null, по умолчанию GET без проверки If-Modified) * @param httpClient HTTP клиент, исполняющий запрос * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @param anyCode загружать содержимое, даже если сервер не вернул HTTP 200, например, html страницы ошибок * (данные будут переданы исключению {@link HttpWrongStatusCodeException}) * @return массив полученных байтов, или NULL, если страница не была изменена (HTTP 304) * @throws IOException ошибка ввода/вывода, в т.ч. если поток прерван отменой задачи (подкласс {@link IOUtils.InterruptedStreamException}) * @throws HttpRequestException если не удалось выполнить запрос, или содержимое отсутствует * @throws HttpWrongStatusCodeException если сервер вернул код не 200. При anycode==true будет содержать также HTML содержимое ответа. */ public byte[] getBytesFromUrl(String url, HttpRequestModel requestModel, HttpClient httpClient, ProgressListener listener, CancellableTask task, boolean anyCode) throws IOException, HttpRequestException, HttpWrongStatusCodeException { HttpResponseModel responseModel = null; try { responseModel = getFromUrl(url, requestModel, httpClient, listener, task); if (responseModel.statusCode == 200) { if (responseModel.stream == null) throw new HttpRequestException(new NullPointerException()); ByteArrayOutputStream output = new ByteArrayOutputStream(1024); IOUtils.copyStream(responseModel.stream, output); return output.toByteArray(); } else { if (responseModel.notModified()) return null; if (anyCode) { byte[] html = null; try { ByteArrayOutputStream output = new ByteArrayOutputStream(1024); IOUtils.copyStream(responseModel.stream, output); html = output.toByteArray(); } catch (Exception e) { Logger.e(TAG, e); } throw new HttpWrongStatusCodeException(responseModel.statusCode, responseModel.statusCode+" - "+responseModel.statusReason, html); } else { throw new HttpWrongStatusCodeException(responseModel.statusCode, responseModel.statusCode+" - "+responseModel.statusReason); } } } catch (Exception e) { if (responseModel != null) removeFromModifiedMap(url); // (responseModel != null) <=> исключение именно во время чтения, а не во время самого запроса (т.е. запись уже устарела в любом случае) throw e; } finally { if (responseModel != null) responseModel.release(); } } /** * HTTP запрос по адресу, получить строку * @param url адрес страницы * @param requestModel модель запроса (может принимать null, по умолчанию GET без проверки If-Modified) * @param httpClient HTTP клиент, исполняющий запрос * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @param anyCode загружать содержимое, даже если сервер не вернул HTTP 200, например, html страницы ошибок * (данные будут переданы исключению {@link HttpWrongStatusCodeException}) * @return полученная строка, или NULL, если страница не была изменена (HTTP 304) * @throws IOException ошибка ввода/вывода, в т.ч. если поток прерван отменой задачи (подкласс {@link IOUtils.InterruptedStreamException}) * @throws HttpRequestException если не удалось выполнить запрос, или содержимое отсутствует * @throws HttpWrongStatusCodeException если сервер вернул код не 200. При anycode==true будет содержать также HTML содержимое ответа. */ public String getStringFromUrl(String url, HttpRequestModel requestModel, HttpClient httpClient, ProgressListener listener, CancellableTask task, boolean anyCode) throws IOException, HttpRequestException, HttpWrongStatusCodeException { byte[] bytes = getBytesFromUrl(url, requestModel, httpClient, listener, task, anyCode); if (bytes == null) return null; return new String(bytes); } /** * HTTP запрос по адресу, получить объект JSON ({@link nya.miku.wishmaster.lib.org_json.JSONObject} актуальная версия org.json) * @param url адрес страницы * @param requestModel модель запроса (может принимать null, по умолчанию GET без проверки If-Modified) * @param httpClient HTTP клиент, исполняющий запрос * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @param anyCode загружать содержимое, даже если сервер не вернул HTTP 200, например, html страницы ошибок * (данные будут переданы исключению {@link HttpWrongStatusCodeException}) * @return объект {@link JSONObject}, или NULL, если страница не была изменена (HTTP 304) * @throws IOException ошибка ввода/вывода, в т.ч. если поток прерван отменой задачи (подкласс {@link IOUtils.InterruptedStreamException}) * @throws HttpRequestException если не удалось выполнить запрос, или содержимое отсутствует * @throws HttpWrongStatusCodeException если сервер вернул код не 200. При anycode==true будет содержать также HTML содержимое ответа. * @throws JSONException в случае ошибки при парсинге JSON */ public JSONObject getJSONObjectFromUrl(String url, HttpRequestModel requestModel, HttpClient httpClient, ProgressListener listener, CancellableTask task, boolean anyCode) throws IOException, HttpRequestException, HttpWrongStatusCodeException, JSONException { return (JSONObject) getJSONFromUrl(url, requestModel, httpClient, listener, task, anyCode, false); } /** * HTTP запрос по адресу, получить массив JSON ({@link nya.miku.wishmaster.lib.org_json.JSONArray} актуальная версия org.json.JSONArray) * @param url адрес страницы * @param requestModel модель запроса (может принимать null, по умолчанию GET без проверки If-Modified) * @param httpClient HTTP клиент, исполняющий запрос * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @param anyCode загружать содержимое, даже если сервер не вернул HTTP 200, например, html страницы ошибок * (данные будут переданы исключению {@link HttpWrongStatusCodeException}) * @return объект {@link JSONArray}, или NULL, если страница не была изменена (HTTP 304) * @throws IOException ошибка ввода/вывода, в т.ч. если поток прерван отменой задачи (подкласс {@link IOUtils.InterruptedStreamException}) * @throws HttpRequestException если не удалось выполнить запрос, или содержимое отсутствует * @throws HttpWrongStatusCodeException если сервер вернул код не 200. При anycode==true будет содержать также HTML содержимое ответа. * @throws JSONException в случае ошибки при парсинге JSON */ public JSONArray getJSONArrayFromUrl(String url, HttpRequestModel requestModel, HttpClient httpClient, ProgressListener listener, CancellableTask task, boolean anyCode) throws IOException, HttpRequestException, HttpWrongStatusCodeException, JSONException { return (JSONArray) getJSONFromUrl(url, requestModel, httpClient, listener, task, anyCode, true); } private Object getJSONFromUrl(String url, HttpRequestModel requestModel, HttpClient httpClient, ProgressListener listener, CancellableTask task, boolean anyCode, boolean isArray) throws IOException, HttpRequestException, HttpWrongStatusCodeException, JSONException { HttpResponseModel responseModel = null; BufferedReader in = null; try { responseModel = getFromUrl(url, requestModel, httpClient, listener, task); if (responseModel.statusCode == 200) { if (responseModel.stream == null) throw new HttpRequestException(new NullPointerException()); in = new BufferedReader(new InputStreamReader(responseModel.stream)); return isArray ? new JSONArray(new JSONTokener(in)) : new JSONObject(new JSONTokener(in)); } else { if (responseModel.notModified()) return null; if (anyCode) { byte[] html = null; try { ByteArrayOutputStream output = new ByteArrayOutputStream(1024); IOUtils.copyStream(responseModel.stream, output); html = output.toByteArray(); } catch (Exception e) { Logger.e(TAG, e); } throw new HttpWrongStatusCodeException(responseModel.statusCode, responseModel.statusCode+" - "+responseModel.statusReason, html); } else { throw new HttpWrongStatusCodeException(responseModel.statusCode, responseModel.statusCode+" - "+responseModel.statusReason); } } } catch (Exception e) { if (responseModel != null) removeFromModifiedMap(url); throw e; } finally { IOUtils.closeQuietly(in); if (responseModel != null) responseModel.release(); } } /** * Скачать файл (HTTP) по заданному адресу URL, записать в поток out. * @param url адрес * @param out целевой поток * @param requestModel модель запроса (может принимать null, по умолчанию GET без проверки If-Modified) * @param httpClient HTTP клиент, исполняющий запрос * @param listener интерфейс отслеживания прогресса (может принимать null) * @param task задача, отмена которой прервёт поток (может принимать null) * @param anyCode загружать содержимое, даже если сервер не вернул HTTP 200, например, html страницы ошибок * (данные будут переданы исключению {@link HttpWrongStatusCodeException}) * @throws IOException ошибка ввода/вывода, в т.ч. если поток прерван отменой задачи (подкласс {@link IOUtils.InterruptedStreamException}) * @throws HttpRequestException если не удалось выполнить запрос, или содержимое отсутствует * @throws HttpWrongStatusCodeException если сервер вернул код не 200. При anycode==true будет содержать также HTML содержимое ответа. */ public void downloadFileFromUrl(String url, OutputStream out, HttpRequestModel requestModel, HttpClient httpClient, ProgressListener listener, CancellableTask task, boolean anyCode) throws IOException, HttpRequestException, HttpWrongStatusCodeException { HttpResponseModel responseModel = null; try { responseModel = getFromUrl(url, requestModel, httpClient, listener, task); if (responseModel.statusCode == 200) { IOUtils.copyStream(responseModel.stream, out); } else { if (anyCode) { byte[] html = null; try { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024); IOUtils.copyStream(responseModel.stream, byteStream); html = byteStream.toByteArray(); } catch (Exception e) { Logger.e(TAG, e); } throw new HttpWrongStatusCodeException(responseModel.statusCode, responseModel.statusCode+" - "+responseModel.statusReason, html); } else { throw new HttpWrongStatusCodeException(responseModel.statusCode, responseModel.statusCode+" - "+responseModel.statusReason); } } } catch (Exception e) { if (responseModel != null) removeFromModifiedMap(url); throw e; } finally { if (responseModel != null) responseModel.release(); } } }