package com.gmail.dpierron.calibre.opds;
/**
* Generate a complete sub-catalog level
*/
import com.gmail.dpierron.calibre.configuration.ConfigurationManager;
import com.gmail.dpierron.calibre.configuration.CustomCatalogEntry;
import com.gmail.dpierron.calibre.configuration.Icons;
import com.gmail.dpierron.calibre.datamodel.*;
import com.gmail.dpierron.calibre.datamodel.filter.BookFilter;
import com.gmail.dpierron.calibre.datamodel.filter.FilterHelper;
import com.gmail.dpierron.tools.i18n.Localization;
import com.gmail.dpierron.tools.Composite;
import com.gmail.dpierron.tools.Helper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.Element;
import java.io.IOException;
import java.util.*;
public class LevelSubCatalog extends SubCatalog {
private final static Logger logger = LogManager.getLogger(LevelSubCatalog.class);
private String title;
public LevelSubCatalog(List<Book> books, String title) {
super(books);
this.title = title;
setStuffToFilterOut(new Vector<Object>() {{add("dummy");}}); // needed to make SubCatalog.isInDeepLevel() know that we're a deep level
}
/**
* Generation of Custom catalogs is broken into its own routine as
* we may want to generate them in front of standard sections, or after
* them depending on configuration settings for each custom catalog.
*
* @param pBreadcrumbs
* @param feed
* @param inSubDir
* @param atTop
* @throws IOException
*/
private void generateCustomCatalogs(
Breadcrumbs pBreadcrumbs,
Element feed,
boolean inSubDir,
boolean atTop)
throws IOException {
Element entry;
if (logger.isDebugEnabled()) logger.debug("STARTED: Generating custom catalogs");
List<CustomCatalogEntry> customCatalogs = currentProfile.getCustomCatalogs();
CatalogManager.callback.startCreateCustomCatalogs(customCatalogs.size());
if (Helper.isNullOrEmpty(customCatalogs)) {
logger.trace("ENDED: No Custom Catalogs set");
} else {
int pos = 1;
Map<String, BookFilter> customCatalogsFilters = CatalogManager.customCatalogsFilters;
for (CustomCatalogEntry customCatalog : customCatalogs) {
CatalogManager.callback.checkIfContinueGenerating();
String customCatalogTitle = customCatalog.getLabel();
String customCatalogSearch = customCatalog.getValue();
logger.trace("Current CustomCatalog is '" + customCatalogTitle + "'");
// check if this custom catalog is wanted in this position?
if (customCatalog.getAtTop() != atTop) {
logger.trace("IGNORED: AtTop setting means skip at the moment.");
continue;
}
if (Helper.isNotNullOrEmpty(customCatalogTitle)
&& (!customCatalogTitle.equals(Constants.CUSTOMCATALOG_DEFAULT_TITLE))
&& (!customCatalogSearch.equals(Constants.CUSTOMCATALOG_DEFAULT_SEARCH))) {
BookFilter customCatalogBookFilter = null;
if (customCatalogsFilters != null) {
customCatalogBookFilter = customCatalogsFilters.get(customCatalogTitle);
}
if (customCatalogBookFilter == null) {
// external catalog
String externalLinkUrl = customCatalog.getValue();
logger.info("Generating custom catalog: " + customCatalogTitle);
if (logger.isDebugEnabled()) logger.debug("Adding external link '" + customCatalogTitle + "', URLValue=" + externalLinkUrl);
boolean opdsLink = false;
// Check if we are linking to an external OPDS library
if (externalLinkUrl.toUpperCase().startsWith(Constants.CUSTOMCATALOG_OPDSURL.toUpperCase())) {
externalLinkUrl = externalLinkUrl.substring(Constants.CUSTOMCATALOG_OPDSURL.length());
opdsLink = true;
// Is this a standard HTML link?
} else if (externalLinkUrl.toUpperCase().startsWith(Constants.CUSTOMCATALOG_HTMLURL.toUpperCase())) {
externalLinkUrl = externalLinkUrl.substring(Constants.CUSTOMCATALOG_HTMLURL.length());
opdsLink = false;
// Assume explicit xml file are OPDS links
} else if (externalLinkUrl.toUpperCase().endsWith(".XML")) {
opdsLink = true;
// Strip off the OPDS part if it precedes a HTTP type URL as this is a special case
if (externalLinkUrl.toUpperCase().startsWith(Constants.CUSTOMCATALOG_OPDSHTTP.toUpperCase())) {
externalLinkUrl = externalLinkUrl.substring(Constants.CUSTOMCATALOG_OPDS.length());
}
}
// Remove any quotes surrounding yje URL
if (externalLinkUrl.length() > 2) {
if (externalLinkUrl.startsWith("\"")) {
externalLinkUrl = externalLinkUrl.substring(1);
}
if (externalLinkUrl.endsWith("\"")) {
externalLinkUrl = externalLinkUrl.substring(0, externalLinkUrl.length()-1);
}
}
// Now add the external link to the feed
entry = FeedHelper.getExternalLinkEntry(customCatalogTitle, Localization.Main.getText("content.externalLink"), opdsLink,
"urn:calibre2opds:externalLink" + (pos++), externalLinkUrl,
currentProfile.getExternalIcons() ? getIconPrefix(inSubDir) + Icons.ICONFILE_EXTERNAL : Icons.ICON_EXTERNAL);
if (entry != null) {
if (currentProfile.getNewWindowForCustomExternalLinks()) {
List<Element> links = entry.getChildren();
if (links != null) {
for (int i =0; i < links.size(); i++) {
Element link = links.get(i);
if (link.getName().equals("link")) {
link.setAttribute("target", "_blank");
break;
}
}
}
}
feed.addContent(entry);
}
} else {
// internal custom catalog (search based)
List<Book> customCatalogBooks = FilterHelper.filter(customCatalogBookFilter, getBooks());
int nb = customCatalogBooks.size();
String s;
switch ((int)nb) {
case 0:
s = Localization.Main.getText("bookword.none", nb);
break;
case 1:
s = Localization.Main.getText("bookword.one", nb);
break;
default:
s = Localization.Main.getText("bookword.many", nb);
break;
}
logger.info("Generating custom catalog: " + customCatalogTitle + " (" + s + ")");
if (Helper.isNullOrEmpty(customCatalogBooks)) {
logger.warn(Localization.Main.getText("error.customCatalogEmpty",customCatalogTitle) + " (" + customCatalogSearch + ")" );
} else {
LevelSubCatalog customSubCatalog = new LevelSubCatalog(customCatalogBooks, customCatalogTitle);
customSubCatalog.setCatalogType(Constants.CUSTOM_TYPE);
customSubCatalog.setCatalogFolder(Constants.CUSTOM_TYPE);
// String customFilename = getCatalogBaseFolderFileName();
// String customUrl = catalogManager.getCatalogFileUrl(customFilename + Constants.XML_EXTENSION, pBreadcrumbs.size() > 1 || inSubDir);
// Breadcrumbs custombreadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, customCatalogTitle, customUrl);
// customSubCatalog.setCatalogLevel(custombreadcrumbs);
customSubCatalog.setCatalogLevel(Breadcrumbs.addBreadcrumb(pBreadcrumbs, customCatalogTitle, null));
customSubCatalog.setCatalogBaseFilename(CatalogManager.getInitialUr());
String customFilename = customSubCatalog.getCatalogBaseFolderFileName();
String customUrl = CatalogManager.getCatalogFileUrl(customFilename + Constants.XML_EXTENSION, pBreadcrumbs.size() > 1 || inSubDir);
Breadcrumbs custombreadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, customCatalogTitle, customUrl);
entry = customSubCatalog.getCatalog(custombreadcrumbs,
null, // No further filter at this point
inSubDir, // Custom catalogs always in subDir
Localization.Main.getText("deeplevel.summary", Summarizer.getBookWord(customCatalogBooks.size())),
"calibre:" + customSubCatalog.getCatalogFolder() + Constants.URN_SEPARATOR + customSubCatalog.getCatalogLevel(),
null,
// Not sure of splitOption at this point
useExternalIcons ? getIconPrefix(inSubDir) + Icons.ICONFILE_CUSTOM : Icons.ICON_CUSTOM);
customSubCatalog = null; // Maybe not necesary - but explicit object cleanup to ensure resources released
if (entry != null) {
feed.addContent(entry);
}
CatalogManager.callback.incStepProgressIndicatorPosition();
}
customCatalogBooks = null;
if (logger.isDebugEnabled()) logger.debug("ENDED: Generating custom catalog " + customCatalogTitle);
}
}
}
if (logger.isDebugEnabled()) logger.debug("COMPLETED: Generating custom catalogs");
CatalogManager.recordRamUsage("After generating Custom Catalogs");
}
CatalogManager.callback.endCreateCustomCatalogs();
CatalogManager.callback.showMessage("");
}
/**
* Generate the standard set of catalog entries
*
* NOTE. If at top level we need to be updating the progress dialog.
* In other caes we do not (maybe this needs revisiting?)
*
* @param pBreadcrumbs
* @param stuffToFilterOut
* @param summary
* @param urn
* @param splitOption
* @param icon
* @param options
* @return
* @throws IOException
*/
public Element getCatalog(Breadcrumbs pBreadcrumbs,
List<Object> stuffToFilterOut,
boolean inSubDir,
String summary,
String urn,
SplitOption splitOption,
String icon,
Option... options) throws IOException {
boolean atTopLevel = (pBreadcrumbs.size() == 0 && getCatalogLevel().length() == 0);
String urlExt = CatalogManager.getCatalogFileUrl(getCatalogBaseFolderFileName() + Constants.XML_EXTENSION, inSubDir);
Element feed = FeedHelper.getFeedRootElement(pBreadcrumbs, title, urn, urlExt, inSubDir || pBreadcrumbs.size() > 1);
// Breadcrumbs breadcrumbs = inSubDir ? Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, urlExt) : pBreadcrumbs;
Breadcrumbs breadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, urlExt);
Composite<Element, String> subCatalogEntry;
Element entry;
/* About entry */
if (atTopLevel) {
if (currentProfile.getIncludeAboutLink()) {
logger.debug("Generating About entry");
entry = FeedHelper.getAboutEntry(Localization.Main.getText("about.title", Constants.PROGTITLE), "urn:calibre2opds:about", Constants.HOME_URL,
Localization.Main.getText("about.summary"), currentProfile.getExternalIcons() ? Icons.ICONFILE_ABOUT : Icons.ICON_ABOUT);
if (entry != null)
feed.addContent(entry);
}
}
/* Featured catalog */
// TODO: Decide if this should be restricted to top level catalog - currently assuming yes?
if (CatalogManager.featuredBooksFilter != null) {
logger.debug("STARTED: Generating Featured catalog");
List<Book> featuredBooks = FilterHelper.filter(CatalogManager.featuredBooksFilter, getBooks());
if (featuredBooks.size() == 0) {
logger.warn("No books found for Featured Books section");
} else {
CatalogManager.callback.setFeaturedCount("" + featuredBooks.size() + " " + Localization.Main.getText("bookword.title"));
FeaturedBooksSubCatalog featuredBooksSubCatalog = new FeaturedBooksSubCatalog(featuredBooks);
if (atTopLevel) CatalogManager.callback.startCreateFeaturedBooks(featuredBooks.size());
featuredBooksSubCatalog.setCatalogLevel(getCatalogLevel());
Element featuredCEntry = featuredBooksSubCatalog.getFeaturedCatalog(breadcrumbs, inSubDir);
featuredBooksSubCatalog = null; // Maybe not necesary - but explicit object cleanup
if (featuredCEntry != null) {
feed.addContent(featuredCEntry);
}
}
logger.debug("COMPLETED: Generating Featured catalog");
if (atTopLevel) CatalogManager.recordRamUsage("After generating Featured Catalog");
}
if (atTopLevel) CatalogManager.callback.endCreateFeaturedBooks();
CatalogManager.callback.checkIfContinueGenerating();
// Custom catalogs when above standard entries
if (atTopLevel) generateCustomCatalogs(breadcrumbs, feed, inSubDir, true);
CatalogManager.callback.checkIfContinueGenerating();
/* Authors */
logger.debug("STARTED: Generating Authors catalog");
if (atTopLevel) CatalogManager.callback.startCreateAuthors(DataModel.getListOfAuthors().size());
if (currentProfile.getGenerateAuthors()) {
AuthorsSubCatalog authorsSubCatalog = new AuthorsSubCatalog(stuffToFilterOut, getBooks());
authorsSubCatalog.setCatalogLevel(getCatalogLevel());
String authorsSummary = "";
if (authorsSubCatalog.getAuthors().size() > 1)
authorsSummary = Localization.Main.getText("authors.alphabetical", authorsSubCatalog.getAuthors().size());
else if (authorsSubCatalog.getAuthors().size() == 1)
authorsSummary = Localization.Main.getText("authors.alphabetical.single");
entry = authorsSubCatalog.getSubCatalog(breadcrumbs,
authorsSubCatalog.getAuthors(),// sTART WITH ALL AUTHORS
inSubDir,
0, // from start,
Localization.Main.getText("authors.title"),
authorsSummary,
Constants.INITIAL_URN_PREFIX + authorsSubCatalog.getCatalogType() + authorsSubCatalog.getCatalogLevel(),
authorsSubCatalog.getCatalogBaseFolderFileName(),
SplitOption.SplitByLetter);
authorsSubCatalog = null; // Maybe not necesary - but explicit object cleanup
if (entry != null)
feed.addContent(entry);
if (atTopLevel) CatalogManager.recordRamUsage("After Generating Author Catalog");
logger.debug("COMPLETED: Generating Authors catalog");
}
if (atTopLevel) CatalogManager.callback.endCreateAuthors();
CatalogManager.callback.checkIfContinueGenerating();
/* Series */
if (atTopLevel) CatalogManager.callback.startCreateSeries(DataModel.getListOfSeries().size());
if (currentProfile.getGenerateSeries()) {
// bug c20-81 Need to allow for (perhaps unlikely) case where no books in library have a series entry set
logger.debug("STARTED: Generating Series catalog");
SeriesSubCatalog seriesSubCatalog = new SeriesSubCatalog(stuffToFilterOut, getBooks());
seriesSubCatalog.setCatalogLevel(getCatalogLevel());
entry = seriesSubCatalog.getSubCatalog(breadcrumbs,
null, // let it be derived from books
atTopLevel != true, // inSubDir
0,
Localization.Main.getText("series.title"),
seriesSubCatalog.getSeries().size() > 1
? Localization.Main.getText("series.alphabetical", seriesSubCatalog.getSeries().size())
: (seriesSubCatalog.getSeries().size() == 1 ? Localization.Main.getText("series.alphabetical.single") : ""),
null, // urn: let it be derived from catalog properties,
null, // filename: let it be derived from catalog properties
SplitOption.SplitByLetter,
false); // seriesWod: Do NOT add to series title
seriesSubCatalog = null; // Maybe not necesary - but explicit object cleanup for earlier resource release
if (entry != null)
feed.addContent(entry);
logger.debug("COMPLETED: Generating Series catalog");
if (atTopLevel) CatalogManager.recordRamUsage("After generating Series catalog");
}
if (atTopLevel) CatalogManager.callback.endCreateSeries();
CatalogManager.callback.checkIfContinueGenerating();
/* Tags */
if (atTopLevel) CatalogManager.callback.startCreateTags(DataModel.getListOfTags().size());
if (currentProfile.getGenerateTags()) {
logger.debug("STARTED: Generating tags catalog");
String SplitTagsOn = currentProfile.getSplitTagsOn();
TagsSubCatalog tagssubCatalog = (currentProfile.getDontSplitTagsOn()
|| Helper.isNullOrEmpty(currentProfile.getSplitTagsOn()))
? new TagListSubCatalog(stuffToFilterOut, getBooks())
: new TagTreeSubCatalog(stuffToFilterOut, getBooks());
tagssubCatalog.setCatalogLevel(getCatalogLevel());
entry = tagssubCatalog.getCatalog(breadcrumbs,
inSubDir);
tagssubCatalog = null; // Maybe not necesary - but explicit object cleanup
if (entry != null)
feed.addContent(entry);
logger.debug("COMPLETED: Generating tags catalog");
if (atTopLevel) CatalogManager.recordRamUsage("After generating Tags catalog");
}
if (atTopLevel) CatalogManager.callback.endCreateTags();
CatalogManager.callback.checkIfContinueGenerating();
/* Recent books */
if (atTopLevel) {
int nbRecentBooks = Math.min(currentProfile.getBooksInRecentAdditions(), DataModel.getListOfBooks().size());
CatalogManager.callback.startCreateRecent(nbRecentBooks);
}
if (currentProfile.getGenerateRecent()) {
logger.debug("STARTED: Generating Recent books catalog");
RecentBooksSubCatalog recentBooksSubCatalog = new RecentBooksSubCatalog(stuffToFilterOut, getBooks());
recentBooksSubCatalog.setCatalogLevel(getCatalogLevel());
entry = recentBooksSubCatalog.getCatalog(breadcrumbs, inSubDir);
recentBooksSubCatalog = null; // Maybe not necesary - but explicit object cleanup
if (entry != null) {
feed.addContent(entry);
}
logger.debug("COMPLETED: Generating Recent books catalog");
if (atTopLevel) CatalogManager.recordRamUsage("After generating Recent catalog");
}
if (atTopLevel) CatalogManager.callback.endCreateRecent();
CatalogManager.callback.checkIfContinueGenerating();
/* Rated books */
if (atTopLevel) CatalogManager.callback.startCreateRated(DataModel.getListOfBooks().size());
if (currentProfile.getGenerateRatings()) {
logger.debug("STARTED: Generating Rated books catalog");
RatingsSubCatalog ratingsSubCatalog = new RatingsSubCatalog(stuffToFilterOut,getBooks());
ratingsSubCatalog.setCatalogLevel(getCatalogLevel());
entry = ratingsSubCatalog.getCatalog(breadcrumbs, inSubDir);
ratingsSubCatalog = null; // Maybe not necesary - but explicit object cleanup
if (entry != null) {
feed.addContent(entry);
}
logger.debug("COMPLETED: Generating Rated books catalog");
if (atTopLevel) CatalogManager.recordRamUsage("After generating Ratings catalog");
}
if (atTopLevel) CatalogManager.callback.endCreateRated();
CatalogManager.callback.checkIfContinueGenerating();
/* All books */
if (atTopLevel) CatalogManager.callback.startCreateAllbooks(DataModel.getListOfBooks().size());
if (currentProfile.getGenerateAllbooks()) {
logger.debug("STARTED: Generating All books catalog");
AllBooksSubCatalog allBooksSubCatalog = new AllBooksSubCatalog(stuffToFilterOut, getBooks());
allBooksSubCatalog.setCatalogLevel(getCatalogLevel());
String allBooksSummary = "";
if (allBooksSubCatalog.getBooks().size() > 1)
allBooksSummary = Localization.Main.getText("allbooks.alphabetical", getBooks().size());
else if (getBooks().size() == 1)
allBooksSummary = Localization.Main.getText("allbooks.alphabetical.single");
entry = allBooksSubCatalog.getListOfBooks(breadcrumbs,
getBooks(),
inSubDir,
0, // from start
Localization.Main.getText("allbooks.title"),
allBooksSummary, Constants.INITIAL_URN_PREFIX + allBooksSubCatalog.getCatalogType(),
allBooksSubCatalog.getCatalogBaseFolderFileName(),
SplitOption.SplitByLetter,
useExternalIcons ? getIconPrefix(inSubDir) + Icons.ICONFILE_BOOKS : Icons.ICON_BOOKS, null);
allBooksSubCatalog = null; // Maybe not necesary - but explicit object cleanup
if (entry != null) {
feed.addContent(entry);
}
logger.debug("COMPLETED: Generating All Books catalog");
if (atTopLevel) CatalogManager.recordRamUsage("After generating All Books sub-catalog");
}
if (atTopLevel) CatalogManager.callback.endCreateAllbooks();
CatalogManager.callback.checkIfContinueGenerating();
/* Custom Catalogs */
if (atTopLevel) {
// TODO: Need to reset data model to allow all books at this point!
generateCustomCatalogs(pBreadcrumbs, feed, inSubDir, false);
}
CatalogManager.callback.checkIfContinueGenerating();
/* Level finished - end-of-level processing */
String outputFilename = getCatalogBaseFolderFileName();
createFilesFromElement( feed,
outputFilename,
(inSubDir || getCatalogLevel().length() > 0 || getCatalogFolder().length() > 0)
? HtmlManager.FeedType.Catalog
: HtmlManager.FeedType.MainCatalog,
false); // Never optimise the index file as we want creation date to be included
// #c2o-214
// A check to see that all cross-reference targets have been generated
// The only reason they would not have been is that the relevant section was suppressed
boolean foundreference;
do {
foundreference = false;
if (!atTopLevel || !currentProfile.getGenerateCrossLinks()){
break;
}
for (Book book : DataModel.getListOfBooks()) {
if (book.isDone() || !book.isReferenced()) {
continue;
}
List<Book> books = Arrays.asList(book);
BooksSubCatalog booksSubCatalog = new BooksSubCatalog(books) {};
booksSubCatalog.setCatalogLevel(getCatalogLevel());
try {
booksSubCatalog.getDetailedEntry(pBreadcrumbs, book);
foundreference = true;
assert book.isDone();
} catch (Exception e) {
logger.error("Error when generating author from book cross-link");
book.setDone();
}
}
for (Author author : DataModel.getListOfAuthors()) {
if (author.isDone() || !author.isReferenced()) {
continue;
}
List<Book> authorBooks = DataModel.getMapOfBooksByAuthor().get(author);
if (authorBooks.size() < 2 && !currentProfile.getSingleBookCrossReferences())
continue;
AuthorsSubCatalog authorsSubCatalog = new AuthorsSubCatalog(authorBooks);
authorsSubCatalog.setCatalogLevel(getCatalogLevel());
try {
authorsSubCatalog.getDetailedEntry(pBreadcrumbs, author, DataModel.getMapOfBooksByAuthor().get(author));
foundreference = true;
assert author.isDone();
} catch (Exception e) {
logger.error("Error when generating author from book cross-link");
author.setDone();
}
}
for(Series series : DataModel.getListOfSeries()){
if (series.isDone() || ! series.isReferenced()) {
continue;
}
SeriesSubCatalog seriesSubCatalog = new SeriesSubCatalog(DataModel.getMapOfBooksBySeries().get(series));
seriesSubCatalog.setCatalogLevel(getCatalogLevel());
try {
seriesSubCatalog.getDetailedEntry(pBreadcrumbs, series, null, Boolean.FALSE);
foundreference = true;
assert series.isDone();
} catch (Exception e) {
logger.error("Error when generating Series from book cross-link");
series.setDone();
}
}
for (Tag tag : DataModel.getListOfTags()) {
if (tag.isDone() || !tag.isReferenced()) {
continue;
}
List<Book> tagBooks = DataModel.getMapOfBooksByTag().get(tag);
if (tagBooks.size() < 2 && !currentProfile.getSingleBookCrossReferences()) {
continue;
}
TagsSubCatalog tagsSubCatalog = new TagListSubCatalog(DataModel.getMapOfBooksByTag().get(tag));
tagsSubCatalog.setCatalogLevel(getCatalogLevel());
try {
tagsSubCatalog.getDetailedEntry(pBreadcrumbs, tag, null, null);
foundreference = true;
assert tag.isDone();
} catch (Exception e) {
logger.error("Error when generating tag from book cross-link");
logger.error(" Exception: " + e);
tag.setDone();
}
}
/*
for (BookRating rating : DataModel.getlistof= book.getRating();
if (!rating.isDone() || isRatingCrossReferences(book)) {
if (! rating.isReferenced()) break;
List<Book> ratingBooks = DataModel.getMapOfBooksByRating().get(rating);
if (isRatingCrossReferences(book)) {
break;
}
RatingsSubCatalog ratingSubCatalog = new RatingsSubCatalog(DataModel.getMapOfBooksByRating().get(rating));
ratingSubCatalog.setCatalogLevel(getCatalogLevel());
try {
ratingSubCatalog.getRatingEntry(null, rating, null);
} catch (Exception e) {
logger.error("Error when generating rating '" + rating.getValue() + "' from book cross-link");
}
}
*/
} while (foundreference);
Element result = FeedHelper.getCatalogEntry(title, urn, CatalogManager.getCatalogFileUrl(outputFilename + Constants.XML_EXTENSION, inSubDir), summary, icon);
return result;
}
public Element getDetailedEntry(Breadcrumbs pBreadcrumbs,
Object obj,
Option... options) throws IOException {
assert false : "getDetailedEntry should never be called";
return null;
}
}