/*
* Copyright 2013 Dmitry Monakhov.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package monakhv.samlib.http;
import java.io.*;
import java.net.Authenticator;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import monakhv.samlib.data.AbstractSettings;
import monakhv.samlib.db.entity.*;
import monakhv.samlib.exception.*;
import monakhv.samlib.log.Log;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import rx.subjects.Subject;
/**
* @author Dmitry Monakhov
* <p>
* The Class make all internet connection for SamLib Info project. Must be call
* from Async tasks or Services only! Have 4 main method
* <p>
* - addAuthor to add new Author to data base. The method is used by AuthorEditorServiceIntent
* <p>
* - getAuthorByURL get Author object using http connection.The method is
* used by Update service
* - downloadBook to download book content to file in
* HTML from. It is used by DownloadBook service
* -searchAuthors used by SearchAuthor async task
*/
public class HttpClientController {
public interface PageReader {
String doReadPage(InputStream in) throws IOException;
}
public static final int RETRY_LIMIT = 5;
public static final int CONNECTION_TIMEOUT = 10000;
public static final int READ_TIMEOUT = 10000;
public static final String ENCODING = "windows-1251";
protected static final String USER_AGENT = "Android reader";
private static final String DEBUG_TAG = "HttpClientController";
private ProxyData mProxyData;
private final SamLibConfig mSamLibConfig;
private final AbstractSettings mSettingsHelper;
private Call mCall;
public HttpClientController(AbstractSettings context) {
mSamLibConfig = SamLibConfig.getInstance(context);
mSettingsHelper = context;
mProxyData = mSettingsHelper.getProxy();
mProxyData = mSettingsHelper.getProxy();
setProxyData(mProxyData);
}
public void cancelAll() {
if (mCall != null) {
mCall.cancel();
}
}
/**
* Construct Author object using reduced.
* URL Internet connection is made using set of mirrors
* <p>
* This is the method for update service
*
* @param link reduced URL
* @param a Clear Author object
* @return Author object
* @throws java.io.IOException
* @throws SamlibParseException
*/
public Author getAuthorByURL(String link, Author a) throws IOException, SamlibParseException, SamlibInterruptException {
a.setUrl(link);
String str = getURL(mSamLibConfig.getAuthorIndexDate(a), new StringReader());
parseAuthorIndexDateData(a, str);
return a;
}
/**
* Create Author object using internet data and reduced url string.
* The same as getAuthorByURL but calculate author name for use in addAuthor task
* Internet connection is made using set of mirrors. This is the method for
* AddAuthor task
*
* @param link reduced url
* @return Author object
* @throws IOException
* @throws SamlibParseException
*/
public Author addAuthor(String link, Author a1) throws IOException, SamlibParseException, SamlibInterruptException {
Author a = getAuthorByURL(link, a1);
a.extractName();
return a;
}
/**
* Save book to appropriate file and make file transformation to make it
* readable by android applications like ALRead and CoolReader.
* Internet connection is made using set of mirrors.
* <p>
* This is the method for DownloadBook service
*
* @param book the book to download
* @throws IOException connection problem occurred
* @throws SamlibParseException remote host return status other then 200
*/
public void downloadBook(Book book, Subject<Integer, Integer> subject) throws IOException, SamlibParseException, SamlibInterruptException {
File f = mSettingsHelper.getBookFile(book, book.getFileType());
PageReader reader;
switch (book.getFileType()) {
case HTML:
reader = new TextFileReader(f, book.getSize(), subject);
getURL(mSamLibConfig.getBookUrl(book), reader);
SamLibConfig.transformBook(f);
break;
case FB2:
reader = new Fb2ZipReader(f, book.getSize(), subject);
getURL(mSamLibConfig.getBookUrl(book), reader);
break;
default:
throw new IOException();
}
}
/**
* Making author search
*
* @param pattern author name pattern to search
* @param page number of page
* @return Search Result
* @throws IOException
* @throws SamlibParseException
*/
public HashMap<String, ArrayList<AuthorCard>> searchAuthors(String pattern, int page) throws IOException, SamlibParseException, SamlibInterruptException {
String str;
try {
str = getURL(mSamLibConfig.getSearchAuthorURL(pattern, page), new StringReader());
} catch (NullPointerException ex) {
Log.w(DEBUG_TAG, "searchAuthors: Search error for pattern: " + pattern, ex);
throw new SamlibParseException("Pattern: " + pattern);
}
return parseSearchAuthorData(str);
}
/**
* Make http connection and begin download data using list of mirrors URL
*
* @param urls list of mirrors URL
* @param reader file to download data to can be null
* @return downloaded data in case file is null
* @throws IOException connection problem
* @throws SamlibParseException remote host return status other then 200
*/
private String getURL(List<String> urls, PageReader reader) throws IOException, SamlibParseException, SamlibInterruptException {
String res = null;
IOException ioException = null;//fatalError
SamlibParseException samlibParseException = null;//skip update for given author
for (String sUrl : urls) {
Log.i(DEBUG_TAG, "getURL: using urls: " + sUrl);
ioException = null;
samlibParseException = null;
try {
URL url = new URL(sUrl);
res = _getURL(url, reader);
} catch (InterruptedIOException e) {
if (Thread.interrupted()) {
Log.i(DEBUG_TAG, "getURL: thread is interrupted throw SamlibInterruptException", e);
throw new SamlibInterruptException("getURL:InterruptedIOException");
}
if (e instanceof SocketTimeoutException) {
mSamLibConfig.flipOrder();
ioException = e;
Log.i(DEBUG_TAG, "getURL: SocketTimeoutException make flip", e);
} else {
Log.i(DEBUG_TAG, "getURL: thread is NOT interrupted throw InterruptedIOException", e);
throw new InterruptedIOException("getURL:InterruptedIOException");
}
} catch (IOException e) {
mSamLibConfig.flipOrder();
ioException = e;
if (Thread.interrupted()) {
Log.i(DEBUG_TAG, "getURL:1 thread is interrupted throw SamlibInterruptException", e);
throw new SamlibInterruptException("getURL:IOException");
}
Log.e(DEBUG_TAG, "getURL: IOException: " + sUrl, e);
} catch (SamlibParseException e) {
mSamLibConfig.flipOrder();
samlibParseException = e;
Log.e(DEBUG_TAG, "AuthorParseException: " + sUrl, e);
}
if (ioException == null && samlibParseException == null) {
return res;
}
}
if (ioException != null) {
throw ioException;
} else {
throw samlibParseException;
}
}
/**
* Row method to make http connection and begin download data Take into
* account 503 return status make retry after one (1) second of sleep. Call
* only by _getURL. Make internal call of __getURL
*
* @param url URL to download from
* @param reader File to download to, can be null
* @return Download data if "f" is null
* @throws IOException connection problem
* @throws SamlibParseException remote host return status other then 200 ad
* 503
*/
private String _getURL(URL url, PageReader reader) throws IOException, SamlibParseException, SamlibInterruptException {
String res = null;
boolean retry = true;
int loopCount = 0;
while (retry) {
try {
res = __getURL(url, reader);
retry = false;
} catch (SamLibIsBusyException ex) {
loopCount++;
Log.w(DEBUG_TAG, "Retry number: " + loopCount + " sleep 1 second");
try {
TimeUnit.SECONDS.sleep(loopCount);
} catch (InterruptedException ex1) {
Log.w(DEBUG_TAG, "_getURL: InterruptedException throw SamlibInterruptException");
throw new SamlibInterruptException("_getURL:Sleep interrupted");
}
if (loopCount >= RETRY_LIMIT) {
// retry = false;
Log.e(DEBUG_TAG, "_getURL: Retry Limit exceeded");
throw new IOException("Retry Limit exceeded");
}
}
}
return res;
}
/**
* Very row method to make http connection and begin download data Call only
* by _getURL
*
* @param url URL to download
* @param reader File to download to can be null
* @return Download data if "f" is null
* @throws IOException connection problem
* @throws SamLibIsBusyException host return 503 status
* @throws SamlibParseException host return status other then 200 and 503
*/
private String __getURL(URL url, PageReader reader) throws IOException, SamLibIsBusyException, SamlibParseException {
final OkHttpClient client;
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS);
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.header("User-Agent", USER_AGENT)
.header("Accept-Charset", ENCODING)
.header("Connection", "close")
.tag(DEBUG_TAG);
if (mProxyData != null) {
mProxyData.applyProxy(builder, requestBuilder);
}
client = builder.build();
Request request = requestBuilder.build();
mCall = client.newCall(request);
Response response;
try {
response = mCall.execute();
Log.d(DEBUG_TAG, "__getURL: Status Response: " + response.message());
} catch (NullPointerException ex) {
Log.e(DEBUG_TAG, "__getURL: Connection Error", ex);
throw new IOException("Connection error: " + url.toString());
}
int status = response.code();
Log.d(DEBUG_TAG, "__getURL: Status - " + status);
if (status == 503) {
throw new SamLibIsBusyException("Need to retryException ");
}
if (status != 200) {
throw new SamlibParseException("URL:" + url.toString() + " status code: " + status);
}
return reader.doReadPage(response.body().byteStream());
}
public void setProxyData(ProxyData proxy1) {
mProxyData = proxy1;
if (proxy1 == null) {
cleanProxy();
}
//Authenticator.setDefault(proxy1.getAuthenticator());
}
private void cleanProxy() {
Authenticator.setDefault(null);
mProxyData = null;
}
/**
* Parse String data to load Author object
*
* @param a Author object to load data to
* @param text String data to parse
*/
private void parseAuthorIndexDateData(Author a, String text) {
String[] lines = text.split("\n");
String authorName = null;
List<GroupBook> groups = new ArrayList<>();
int iBooks = 0;
for (String line : lines) {
Matcher nameMatcher = SamLibConfig.AUTHOR_NAME_PATTERN.matcher(line);
Matcher bookMatcher = SamLibConfig.BOOK_PATTERN.matcher(line);
if ((authorName == null) && nameMatcher.find()) {
authorName = nameMatcher.group(1);
Log.i(DEBUG_TAG, "Name = " + authorName);
}
if (bookMatcher.find()) {
++iBooks;
Book book = new Book(a, bookMatcher);
GroupBook g = book.getGroupBook();
if (!groups.contains(g)) {
groups.add(g);
}
a.getBooks().add(book);
if (authorName != null) {
book.setAuthorName(authorName);
}
}
//Log.i(DEBUG_TAG,line);
}
a.setGroupBooks(groups);
Log.i(DEBUG_TAG, "Books = " + iBooks);
}
private HashMap<String, ArrayList<AuthorCard>> parseSearchAuthorData(String text) throws SamlibParseException {
String[] lines = text.split("\n");
HashMap<String, ArrayList<AuthorCard>> res = new HashMap<>();
for (String line : lines) {
if (SamLibConfig.testSplit(line) < 7) {
Log.e(DEBUG_TAG, "Line Search parse Error: length=" + SamLibConfig.testSplit(line) + "\nline: " + line + "\nlines: " + lines.length);
throw new SamlibParseException("Parse Search Author error\nline: " + line);
}
try {
AuthorCard card = new AuthorCard(line);
String name = card.getName();
if (res.containsKey(name)) {
res.get(name).add(card);
} else {
ArrayList<AuthorCard> aa = new ArrayList<>();
aa.add(card);
res.put(name, aa);
}
} catch (SamLibNullAuthorException ex) {
//Log.i(DEBUG_TAG,"Skip author with no book");
}
}
if (res.isEmpty()) {
return null;
}
return res;
}
}