/* * Copyright (C) 2010-2011 Geometer Plus <contact@geometerplus.com> * * 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 2 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ package org.geometerplus.fbreader.network.opds; import java.util.*; import org.geometerplus.zlibrary.core.constants.MimeTypes; import org.geometerplus.zlibrary.core.util.ZLNetworkUtil; import org.geometerplus.fbreader.network.*; import org.geometerplus.fbreader.network.atom.*; import org.geometerplus.fbreader.network.authentication.litres.LitResBookshelfItem; import org.geometerplus.fbreader.network.authentication.litres.LitResRecommendationsItem; class NetworkOPDSFeedReader implements OPDSFeedReader, OPDSConstants, MimeTypes { private final String myBaseURL; private final OPDSCatalogItem.State myData; private int myIndex; private String myNextURL; private String mySkipUntilId; private boolean myFoundNewIds; private int myItemsToLoad = -1; /** * Creates new OPDSFeedReader instance that can be used to get NetworkItem objects from OPDS feeds. * * @param baseURL string that contains URL of the OPDS feed, that will be read using this instance of the reader * @param result network results buffer. Must be created using OPDSNetworkLink corresponding to the OPDS feed, * that will be read using this instance of the reader. */ NetworkOPDSFeedReader(String baseURL, OPDSCatalogItem.State result) { myBaseURL = baseURL; myData = result; mySkipUntilId = myData.LastLoadedId; myFoundNewIds = mySkipUntilId != null; if (!(result.Link instanceof OPDSNetworkLink)) { throw new IllegalArgumentException("Parameter `result` has invalid `Link` field value: result.Link must be an instance of OPDSNetworkLink class."); } } public void processFeedStart() { myData.ResumeURI = myBaseURL; } public boolean processFeedMetadata(OPDSFeedMetadata feed, boolean beforeEntries) { if (beforeEntries) { myIndex = feed.OpensearchStartIndex - 1; if (feed.OpensearchItemsPerPage > 0) { myItemsToLoad = feed.OpensearchItemsPerPage; final int len = feed.OpensearchTotalResults - myIndex; if (len > 0 && len < myItemsToLoad) { myItemsToLoad = len; } } return false; } final OPDSNetworkLink opdsLink = (OPDSNetworkLink)myData.Link; for (ATOMLink link: feed.Links) { final String type = ZLNetworkUtil.filterMimeType(link.getType()); final String rel = opdsLink.relation(link.getRel(), type); if (MIME_APP_ATOM.equals(type) && "next".equals(rel)) { myNextURL = ZLNetworkUtil.url(myBaseURL, link.getHref()); } } return false; } public void processFeedEnd() { if (mySkipUntilId != null) { // Last loaded element was not found => resume error => DO NOT RESUME // TODO: notify user about error??? // TODO: do reload??? myNextURL = null; } myData.ResumeURI = myFoundNewIds ? myNextURL : null; myData.LastLoadedId = null; } // returns BookReference.Format value for specified String. String MUST BE interned. private static int formatByMimeType(String mimeType) { if (MIME_APP_FB2ZIP.equals(mimeType)) { return BookReference.Format.FB2_ZIP; } else if (MIME_APP_EPUB.equals(mimeType)) { return BookReference.Format.EPUB; } else if (MIME_APP_MOBI.equals(mimeType)) { return BookReference.Format.MOBIPOCKET; } return BookReference.Format.NONE; } // returns BookReference.Type value for specified String. String MUST BE interned. private static int typeByRelation(String rel) { if (rel == null || REL_ACQUISITION.equals(rel) || REL_ACQUISITION_OPEN.equals(rel)) { return BookReference.Type.DOWNLOAD_FULL; } else if (REL_ACQUISITION_SAMPLE.equals(rel)) { return BookReference.Type.DOWNLOAD_DEMO; } else if (REL_ACQUISITION_CONDITIONAL.equals(rel)) { return BookReference.Type.DOWNLOAD_FULL_CONDITIONAL; } else if (REL_ACQUISITION_SAMPLE_OR_FULL.equals(rel)) { return BookReference.Type.DOWNLOAD_FULL_OR_DEMO; } else if (REL_ACQUISITION_BUY.equals(rel)) { return BookReference.Type.BUY; } else { return BookReference.Type.UNKNOWN; } } private boolean tryInterrupt() { final int noninterruptableRemainder = 10; return (myItemsToLoad < 0 || myItemsToLoad > noninterruptableRemainder) && myData.Listener.confirmInterrupt(); } private String calculateEntryId(OPDSEntry entry) { if (entry.Id != null) { return entry.Id.Uri; } String id = null; int idType = 0; final OPDSNetworkLink opdsLink = (OPDSNetworkLink)myData.Link; for (ATOMLink link: entry.Links) { final String type = ZLNetworkUtil.filterMimeType(link.getType()); final String rel = opdsLink.relation(link.getRel(), type); if (rel == null && MIME_APP_ATOM.equals(type)) { return ZLNetworkUtil.url(myBaseURL, link.getHref()); } int relType = BookReference.Format.NONE; if (rel == null || rel.startsWith(REL_ACQUISITION_PREFIX) || rel.startsWith(REL_FBREADER_ACQUISITION_PREFIX)) { relType = formatByMimeType(type); } if (relType != BookReference.Format.NONE && (id == null || idType < relType || (idType == relType && REL_ACQUISITION.equals(rel)))) { id = ZLNetworkUtil.url(myBaseURL, link.getHref()); idType = relType; } } return id; } public boolean processFeedEntry(OPDSEntry entry) { if (myItemsToLoad >= 0) { --myItemsToLoad; } if (entry.Id == null) { final String id = calculateEntryId(entry); if (id == null) { return tryInterrupt(); } entry.Id = new ATOMId(); entry.Id.Uri = id; } if (mySkipUntilId != null) { if (mySkipUntilId.equals(entry.Id.Uri)) { mySkipUntilId = null; } return tryInterrupt(); } myData.LastLoadedId = entry.Id.Uri; if (!myFoundNewIds && !myData.LoadedIds.contains(entry.Id.Uri)) { myFoundNewIds = true; } myData.LoadedIds.add(entry.Id.Uri); final OPDSNetworkLink opdsLink = (OPDSNetworkLink)myData.Link; boolean hasBookLink = false; for (ATOMLink link: entry.Links) { final String type = ZLNetworkUtil.filterMimeType(link.getType()); final String rel = opdsLink.relation(link.getRel(), type); if (rel == null ? (formatByMimeType(type) != BookReference.Format.NONE) : (rel.startsWith(REL_ACQUISITION_PREFIX) || rel.startsWith(REL_FBREADER_ACQUISITION_PREFIX))) { hasBookLink = true; break; } } NetworkItem item; if (hasBookLink) { item = readBookItem(entry); } else { item = readCatalogItem(entry); } if (item != null) { myData.Listener.onNewItem(myData.Link, item); } return tryInterrupt(); } private static final String AuthorPrefix = "author:"; private static final String AuthorsPrefix = "authors:"; private NetworkItem readBookItem(OPDSEntry entry) { final OPDSNetworkLink opdsNetworkLink = (OPDSNetworkLink)myData.Link; /*final String date; if (entry.DCIssued != null) { date = entry.DCIssued.getDateTime(true); } else { date = null; }*/ final LinkedList<String> tags = new LinkedList<String>(); for (ATOMCategory category: entry.Categories) { String label = category.getLabel(); if (label == null) { label = category.getTerm(); } if (label != null) { tags.add(label); } } String cover = null; LinkedList<BookReference> references = new LinkedList<BookReference>(); for (ATOMLink link: entry.Links) { final String href = ZLNetworkUtil.url(myBaseURL, link.getHref()); final String type = ZLNetworkUtil.filterMimeType(link.getType()); final String rel = opdsNetworkLink.relation(link.getRel(), type); final int referenceType = typeByRelation(rel); if (REL_IMAGE_THUMBNAIL.equals(rel) || REL_THUMBNAIL.equals(rel)) { if (MIME_IMAGE_PNG.equals(type) || MIME_IMAGE_JPEG.equals(type)) { cover = href; } } else if ((rel != null && rel.startsWith(REL_IMAGE_PREFIX)) || REL_COVER.equals(rel)) { if (cover == null && (MIME_IMAGE_PNG.equals(type) || MIME_IMAGE_JPEG.equals(type))) { cover = href; } } else if (BookReference.Type.BUY == referenceType) { final OPDSLink opdsLink = (OPDSLink)link; String price = null; final OPDSPrice opdsPrice = opdsLink.selectBestPrice(); if (opdsPrice != null) { price = BuyBookReference.price(opdsPrice.Price, opdsPrice.Currency); } if (price == null) { // FIXME: HACK: price handling must be implemented not through attributes!!! price = BuyBookReference.price(entry.getAttribute(OPDSXMLReader.KEY_PRICE), null); } if (price == null) { price = ""; } if (MIME_TEXT_HTML.equals(type)) { collectReferences(references, opdsLink, href, BookReference.Type.BUY_IN_BROWSER, price, true); } else { collectReferences(references, opdsLink, href, BookReference.Type.BUY, price, false); } } else if (referenceType != BookReference.Type.UNKNOWN) { final int format = formatByMimeType(type); if (format != BookReference.Format.NONE) { references.add(new BookReference(href, format, referenceType)); } } } LinkedList<NetworkBookItem.AuthorData> authors = new LinkedList<NetworkBookItem.AuthorData>(); for (ATOMAuthor author: entry.Authors) { String name = author.Name; final String lowerCased = name.toLowerCase(); int index = lowerCased.indexOf(AuthorPrefix); if (index != -1) { name = name.substring(index + AuthorPrefix.length()); } else { index = lowerCased.indexOf(AuthorsPrefix); if (index != -1) { name = name.substring(index + AuthorsPrefix.length()); } } index = name.indexOf(','); NetworkBookItem.AuthorData authorData; if (index != -1) { final String before = name.substring(0, index).trim(); final String after = name.substring(index + 1).trim(); authorData = new NetworkBookItem.AuthorData(after + ' ' + before, before); } else { name = name.trim(); index = name.lastIndexOf(' '); authorData = new NetworkBookItem.AuthorData(name, name.substring(index + 1)); } authors.add(authorData); } //entry.dcPublisher(); //entry.updated(); //entry.published(); /*for (size_t i = 0; i < entry.contributors().size(); ++i) { ATOMContributor &contributor = *(entry.contributors()[i]); std::cerr << "\t\t<contributor>" << std::endl; std::cerr << "\t\t\t<name>" << contributor.name() << "</name>" << std::endl; if (!contributor.uri().empty()) std::cerr << "\t\t\t<uri>" << contributor.uri() << "</uri>" << std::endl; if (!contributor.email().empty()) std::cerr << "\t\t\t<email>" << contributor.email() << "</email>" << std::endl; std::cerr << "\t\t</contributor>" << std::endl; }*/ //entry.rights(); final String annotation; if (entry.Summary != null) { annotation = entry.Summary; } else if (entry.Content != null) { annotation = entry.Content; } else { annotation = null; } return new NetworkBookItem( opdsNetworkLink, entry.Id.Uri, myIndex++, entry.Title, annotation, //entry.DCLanguage, //date, authors, tags, entry.SeriesTitle, entry.SeriesIndex, cover, references ); } private void collectReferences(LinkedList<BookReference> references, OPDSLink opdsLink, String href, int type, String price, boolean addWithoutFormat) { boolean added = false; for (String mime: opdsLink.Formats) { final int format = formatByMimeType(mime); if (format != BookReference.Format.NONE) { references.add(new BuyBookReference( href, format, type, price )); added = true; } } if (!added && addWithoutFormat) { references.add(new BuyBookReference( href, BookReference.Format.NONE, type, price )); } } private NetworkItem readCatalogItem(OPDSEntry entry) { final OPDSNetworkLink opdsLink = (OPDSNetworkLink)myData.Link; String coverURL = null; String url = null; boolean urlIsAlternate = false; String htmlURL = null; String litresRel = null; int catalogType = NetworkCatalogItem.FLAGS_DEFAULT; for (ATOMLink link : entry.Links) { final String href = ZLNetworkUtil.url(myBaseURL, link.getHref()); final String type = ZLNetworkUtil.filterMimeType(link.getType()); final String rel = opdsLink.relation(link.getRel(), type); if (MIME_IMAGE_PNG.equals(type) || MIME_IMAGE_JPEG.equals(type)) { if (REL_IMAGE_THUMBNAIL.equals(rel) || REL_THUMBNAIL.equals(rel) || (coverURL == null && (REL_COVER.equals(rel) || (rel != null && rel.startsWith(REL_IMAGE_PREFIX))))) { coverURL = href; } } else if (MIME_APP_ATOM.equals(type)) { if (ATOMConstants.REL_ALTERNATE.equals(rel)) { if (url == null) { url = href; urlIsAlternate = true; } } else if (url == null || rel == null || rel.equals(REL_SUBSECTION)) { url = href; urlIsAlternate = false; if (REL_CATALOG_AUTHOR.equals(rel)) { catalogType &= ~NetworkCatalogItem.FLAG_SHOW_AUTHOR; } else if (REL_CATALOG_SERIES.equals(rel)) { catalogType &= ~NetworkCatalogItem.FLAGS_GROUP; } } } else if (MIME_TEXT_HTML.equals(type)) { if (REL_ACQUISITION.equals(rel) || REL_ACQUISITION_OPEN.equals(rel) || ATOMConstants.REL_ALTERNATE.equals(rel) || rel == null) { htmlURL = href; } } else if (MIME_APP_LITRES.equals(type)) { litresRel = rel; url = href; } } if (url == null && htmlURL == null) { return null; } if (url != null && !urlIsAlternate) { htmlURL = null; } final String annotation; if (entry.Summary != null) { annotation = entry.Summary.replace("\n", ""); } else if (entry.Content != null) { annotation = entry.Content.replace("\n", ""); } else { annotation = null; } HashMap<Integer,String> urlMap = new HashMap<Integer,String>(); if (url != null) { urlMap.put(NetworkURLCatalogItem.URL_CATALOG, url); } if (htmlURL != null) { urlMap.put(NetworkURLCatalogItem.URL_HTML_PAGE, htmlURL); } if (litresRel != null) { if (REL_BOOKSHELF.equals(litresRel)) { return new LitResBookshelfItem( opdsLink, entry.Title, annotation, coverURL, urlMap, opdsLink.getCondition(entry.Id.Uri) ); } else if (REL_RECOMMENDATIONS.equals(litresRel)) { return new LitResRecommendationsItem( opdsLink, entry.Title, annotation, coverURL, urlMap, opdsLink.getCondition(entry.Id.Uri) ); } else if (REL_BASKET.equals(litresRel)) { return null; /* return new BasketItem( opdsLink, entry.Title, annotation, coverURL, urlMap, opdsLink.getCondition(entry.Id.Uri) ); */ } else if (REL_TOPUP.equals(litresRel)) { return new TopUpItem(opdsLink, coverURL); } else { return null; } } else { return new OPDSCatalogItem( opdsLink, entry.Title, annotation, coverURL, urlMap, opdsLink.getCondition(entry.Id.Uri), catalogType ); } } }