package org.ebookdroid.opds;
import org.ebookdroid.CodecType;
import org.sufficientlysecure.viewer.R;
import org.ebookdroid.common.settings.OpdsSettings;
import org.ebookdroid.opds.exceptions.AuthorizationRequiredException;
import org.ebookdroid.opds.exceptions.HttpRequestFailed;
import org.ebookdroid.opds.exceptions.OPDSException;
import org.ebookdroid.opds.model.Book;
import org.ebookdroid.opds.model.BookDownloadLink;
import org.ebookdroid.opds.model.Feed;
import org.ebookdroid.opds.model.Link;
import org.ebookdroid.opds.xml.OPDSContentHandler;
import android.annotation.TargetApi;
import android.webkit.URLUtil;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.ClosedByInterruptException;
import java.util.Enumeration;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicHeader;
import org.emdev.BaseDroidApp;
import org.emdev.common.archives.zip.ZipArchive;
import org.emdev.common.archives.zip.ZipArchiveEntry;
import org.emdev.common.cache.CacheManager;
import org.emdev.common.http.BaseHttpClient;
import org.emdev.common.log.LogContext;
import org.emdev.common.log.LogManager;
import org.emdev.ui.progress.IProgressIndicator;
import org.emdev.ui.progress.UIFileCopying;
import org.emdev.utils.LengthUtils;
import org.emdev.utils.MathUtils;
@TargetApi(8)
public class OPDSClient extends BaseHttpClient {
private static final LogContext LCTX = LogManager.root().lctx("OPDS");
private final IEntryBuilder builder;
public OPDSClient(final IEntryBuilder builder) {
super(BaseDroidApp.APP_PACKAGE + " " + BaseDroidApp.APP_VERSION_NAME);
this.builder = builder;
}
@Override
protected void finalize() {
close();
}
public Feed loadFeed(final Feed feed, final IProgressIndicator progress) throws OPDSException {
if (feed.link == null) {
return feed;
}
final Header h1 = new BasicHeader("Accept", "text/html,application/xhtml+xml,application/xml");
final Header h2 = new BasicHeader("Accept-Charset", "UTF-8");
try {
final AtomicReference<URI> uri = createURI(feed.parent, feed.link.uri);
if (feed.parent == null && LengthUtils.isNotEmpty(feed.login)) {
setAuthorization(uri.get().getHost(), feed.login, feed.password);
}
final HttpResponse resp = connect(uri, h1, h2);
final StatusLine statusLine = resp.getStatusLine();
final int statusCode = statusLine.getStatusCode();
if (statusCode != 200) {
LCTX.e("Content cannot be retrived: " + statusLine);
throw new HttpRequestFailed(statusLine);
}
progress.setProgressDialogMessage(R.string.opds_loading_catalog);
final HttpEntity entity = resp.getEntity();
final Header enc = entity.getContentEncoding();
final String encoding = LengthUtils.safeString(enc != null ? enc.getValue() : "", "UTF-8");
final BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent(), encoding),
MathUtils.adjust((int) entity.getContentLength(), 4 * 1024, 64 * 1024));
final StringBuilder buf = new StringBuilder();
for (String s = in.readLine(); s != null; s = in.readLine()) {
buf.append(s).append("\n");
}
in.close();
// System.out.println(buf);
// Workaround for Calibre (tested with 0.8.38)
if (buf.indexOf("<title>..:: calibre library ::.. </title>") >= 0) {
LCTX.w("The feed link is calibre root URI: " + feed.link.uri);
feed.link = new Link(feed.link.uri + "/opds");
return loadFeed(feed, progress);
}
final OPDSContentHandler h = new OPDSContentHandler(feed, builder);
h.parse(buf.toString());
} catch (final InterruptedIOException ex) {
LCTX.e("Error on OPDS catalog access: " + ex.getMessage());
throw new OPDSException(ex);
} catch (final OPDSException ex) {
LCTX.e("Error on OPDS catalog access: " + ex.getMessage());
throw ex;
} catch (final Throwable th) {
LCTX.e("Error on OPDS catalog access: ", th);
throw new OPDSException(th);
}
feed.loadedAt = System.currentTimeMillis();
return feed;
}
public File downloadBook(final Book book, final BookDownloadLink link, final IProgressIndicator progress)
throws OPDSException {
try {
final AtomicReference<URI> uri = createURI(book.parent, link.uri);
final HttpResponse resp = connect(uri);
final StatusLine statusLine = resp.getStatusLine();
final int statusCode = statusLine.getStatusCode();
if (statusCode != 200) {
LCTX.e(Thread.currentThread().getName() + ": Content cannot be retrived: " + statusLine);
throw new HttpRequestFailed(statusLine);
}
final HttpEntity entity = resp.getEntity();
final String contentDisposition = getHeaderValue(resp, "Content-Disposition");
final String mimeType = getHeaderValue(entity.getContentType());
final long contentLength = entity.getContentLength();
LCTX.d(Thread.currentThread().getName() + ": Content-Disposition: " + contentDisposition);
LCTX.d(Thread.currentThread().getName() + ": Content-Type: " + mimeType);
LCTX.d(Thread.currentThread().getName() + ": Content-Length: " + contentLength);
final String guessFileName = URLUtil.guessFileName(uri.get().toString(),
contentDisposition.replace("attachement", "attachment"), mimeType);
LCTX.d(Thread.currentThread().getName() + ": File name: " + guessFileName);
// create a new file, specifying the path, and the filename which we want to save the file as.
final File downloadDir = new File(OpdsSettings.current().downloadDir);
if (!downloadDir.exists()) {
downloadDir.mkdirs();
}
final File file = new File(downloadDir, guessFileName);
final File tmpFile = new File(downloadDir, guessFileName + ".part");
final boolean exists = file.exists() && file.length() == contentLength;
// this will be used to write the downloaded data into the file we created
try {
if (!exists) {
final UIFileCopying worker = new UIFileCopying(R.string.opds_loading_book, 64 * 1024, progress);
final BufferedInputStream input = new BufferedInputStream(entity.getContent(), 64 * 1024);
final BufferedOutputStream tmpFileOutput = new BufferedOutputStream(new FileOutputStream(tmpFile),
256 * 1024);
worker.copy(contentLength, input, tmpFileOutput);
tmpFile.renameTo(file);
}
if (OpdsSettings.current().unpackArchives && link.isZipped && !link.bookType.isZipSupported()) {
return unpack(file, progress);
}
} catch (final ClosedByInterruptException ex) {
try {
file.delete();
tmpFile.delete();
} catch (final Exception ex1) {
}
}
return file;
} catch (final OPDSException ex) {
throw ex;
} catch (final IOException ex) {
LCTX.e(Thread.currentThread().getName() + ": Error on downloading book: " + ex.getMessage());
throw new OPDSException(ex);
} catch (final Throwable th) {
LCTX.e(Thread.currentThread().getName() + ": Error on downloading book: ", th);
throw new OPDSException(th);
}
}
public File loadFile(final Feed parent, final Link link) {
try {
final AtomicReference<URI> uriRef = createURI(parent, link.uri);
final HttpResponse resp = connect(uriRef);
final HttpEntity entity = resp.getEntity();
return CacheManager.createTempFile(entity.getContent(), ".opds");
} catch (final ClosedByInterruptException ex) {
Thread.interrupted();
LCTX.w(Thread.currentThread().getName() + ": Thumbnail loading interrupted");
} catch (final InterruptedIOException ex) {
Thread.interrupted();
LCTX.w(Thread.currentThread().getName() + ": Thumbnail loading interrupted");
} catch (final AuthorizationRequiredException ex) {
// No thumbnails without authentication
} catch (final OPDSException ex) {
LCTX.e(Thread.currentThread().getName() + ": Error on OPDS thumbnail loading: ", ex.getCause());
} catch (final Throwable th) {
LCTX.e(Thread.currentThread().getName() + ": Error on OPDS catalog access: ", th);
}
return null;
}
protected AtomicReference<URI> createURI(final Feed parent, String uri) throws URISyntaxException {
URI reqUri = new URI(uri);
if (reqUri.getHost() == null) {
for (Feed p = parent; p != null; p = p.parent) {
final URI parentURI = new URI(p.link.uri);
if (parentURI.isAbsolute()) {
reqUri = parentURI.resolve(reqUri);
uri = reqUri.toASCIIString();
break;
}
}
}
return new AtomicReference<URI>(reqUri);
}
protected File unpack(final File file, final IProgressIndicator progress) {
try {
final ZipArchive archive = new ZipArchive(file);
try {
final Enumeration<ZipArchiveEntry> entries = archive.entries();
while (entries.hasMoreElements()) {
final ZipArchiveEntry entry = entries.nextElement();
final CodecType codecType = CodecType.getByUri(entry.getName());
if (codecType != null) {
final File entryFile = new File(file.getParentFile(), entry.getName());
LCTX.d(Thread.currentThread().getName() + ": Unpacked file name: " + entryFile.getAbsolutePath());
final int bufsize = 256 * 1024;
final UIFileCopying worker = new UIFileCopying(R.string.opds_unpacking_book, bufsize, progress);
final BufferedInputStream in = new BufferedInputStream(entry.open(), bufsize);
final BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(entryFile),
bufsize);
worker.copy(entry.getSize(), in, out);
release(file, archive);
return entryFile;
}
}
} catch (final ClosedByInterruptException ex) {
release(file, archive);
} catch (final Exception ex) {
LCTX.e(Thread.currentThread().getName() + ": Error on unpacking book: ", ex);
try {
archive.close();
} catch (final Exception ex1) {
}
}
} catch (final Exception ex) {
LCTX.e(Thread.currentThread().getName() + ": Error on unpacking book: ", ex);
}
return file;
}
protected void release(final File file, final ZipArchive archive) {
try {
archive.close();
} catch (final Exception ex1) {
}
if (OpdsSettings.current().deleteArchives) {
try {
file.delete();
} catch (final Exception ex) {
}
}
}
}