package com.gmail.dpierron.calibre.opds; /** * Class for implementing the Author sub-catalogs. * Inherits from: * -> BooksSubcatalog - methods for listing contained books. * -> SubCatalog */ import com.gmail.dpierron.calibre.configuration.Icons; import com.gmail.dpierron.calibre.configuration.ConfigurationManager; import com.gmail.dpierron.calibre.datamodel.Author; import com.gmail.dpierron.calibre.datamodel.Book; import com.gmail.dpierron.calibre.datamodel.DataModel; import com.gmail.dpierron.calibre.datamodel.Series; import com.gmail.dpierron.tools.i18n.Localization; import com.gmail.dpierron.calibre.trook.TrookSpecificSearchDatabaseManager; 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.text.Collator; import java.util.*; public class AuthorsSubCatalog extends BooksSubCatalog { private final static Logger logger = LogManager.getLogger(AuthorsSubCatalog.class); private final static Collator collator = Collator.getInstance(ConfigurationManager.getLocale()); private Map<Author, List<Book>> mapOfBooksByAuthor; // Cached information for efficency private List<Author> authors; // Cached information for efficiency // CONSTRUCTORS public AuthorsSubCatalog(List<Object> stuffToFilterOut, List<Book> books) { super(stuffToFilterOut, books); setCatalogType(Constants.AUTHORLIST_TYPE); initMapOfBooksByAuthor(); // Force initialisation } public AuthorsSubCatalog(List<Book> books) { super(books); setCatalogType(Constants.AUTHORLIST_TYPE); initMapOfBooksByAuthor(); // Force initialisation } /** * Build up the list of book/author relationships * We cache the results for improved efficiency on indivisual authors. * * @return */ private void initMapOfBooksByAuthor() { mapOfBooksByAuthor = new HashMap<Author, List<Book>>(); authors = new LinkedList<Author>(); for (Book book : getBooks()) { assert book.getAuthors() != null; for (Author author : book.getAuthors()) { List<Book> currentbooks = mapOfBooksByAuthor.get(author); if (currentbooks == null) { currentbooks = new LinkedList<Book>(); mapOfBooksByAuthor.put(author, currentbooks); } currentbooks.add(book); if (!authors.contains(author)) authors.add(author); } } // sort the authors by name // We can use configuration parameters to sort by either auth_sort or author Collections.sort(authors, new Comparator<Author>() { public int compare(Author o1, Author o2) { String name1 = (o1 == null ? "" : o1.getTitleToSplitByLetter()); String name2 = (o2 == null ? "" : o2.getTitleToSplitByLetter()); return Helper.checkedCollatorCompareIgnoreCase(name1, name2,collator); } }); } /** * Produce a list of authors. * This function is used recursively to handle a set of pages * * @param pBreadcrumbs The point we have reached so far * @param listauthors The list of authors that need listing * @param inSubDir * @param from The point reached in the list. Will be 0 first time through * @param title The title for this page * @param summary THe summary * @param urn The URN to link back to the calling point * @param pFilename The filename to be used as the bawe for this set of pages * @param splitOption The current preferred split option. * @return Link to the page just generated to insert into parent * @throws IOException */ public Element getSubCatalog(Breadcrumbs pBreadcrumbs, List<Author> listauthors, boolean inSubDir, int from, String title, String summary, String urn, String pFilename, SplitOption splitOption) throws IOException { if (from != 0) inSubDir = true; int pageNumber = Summarizer.getPageNumber(from + 1); String filename = pFilename + Constants.PAGE_DELIM + Integer.toString(pageNumber); String urlExt = CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, inSubDir); Element feed = FeedHelper.getFeedRootElement(pBreadcrumbs, title, urn, urlExt, true /* inSubDir*/); // Update breadcrumbs to point to this list Breadcrumbs breadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, urlExt); // Check for special case of all entries being identical last name so we cannot split further regardless of split trigger value if (listauthors == null || listauthors.size() == 0) { int dummy = 1; } assert (listauthors != null && listauthors.size() > 0); String lastName = listauthors.get(0).getLastName().toUpperCase(); // Get name of first entry boolean willSplitByLetter = false; for (Author author : listauthors) { // debug if (! author.getLastName().toUpperCase().equals(lastName)) { // As long as entries are not all the same, apply the split criteria willSplitByLetter = checkSplitByLetter(splitOption,listauthors.size()); break; } } // Check for special case where the author sort name is equal to the split level. while ( willSplitByLetter && listauthors.size() > 0 && pFilename.toUpperCase().endsWith(Constants.TYPE_SEPARATOR + listauthors.get(0).getNameForSort().toUpperCase())) { Author author = listauthors.get(0); listauthors.remove(0); Element element; element = getDetailedEntry(breadcrumbs, author, mapOfBooksByAuthor.get(author)); assert element != null; if (element != null) { feed.addContent(element); } willSplitByLetter = checkSplitByLetter(splitOption,listauthors.size()); } Map<String, List<Author>> mapOfAuthorsByLetter = null; int catalogSize; if (willSplitByLetter) { mapOfAuthorsByLetter = DataModel.splitAuthorsByLetter(listauthors); catalogSize = 0; } else { catalogSize = listauthors.size(); } int maxPages = Summarizer.getPageNumber(catalogSize); logger.debug("generating " + urlExt); // list the entries (or split them) List<Element> result; if (willSplitByLetter /* listauthors.size() > 1*/) { logger.debug("splitting by letter"); result = getListOfAuthorsSplitByLetter(breadcrumbs, mapOfAuthorsByLetter, title, urn, pFilename); } else { logger.debug("NOT splitting by letter"); result = new LinkedList<Element>(); for (int i = from; i < listauthors.size(); i++) { if ((splitOption != SplitOption.DontSplitNorPaginate) && ((i - from) >= maxBeforePaginate)) { // TODO #c2o-208 Add Previous, First and Last links if needed // Get a new page Element nextLink = getSubCatalog(breadcrumbs, listauthors, true, i, title, summary, urn, pFilename, splitOption != SplitOption.DontSplitNorPaginate ? SplitOption.Paginate : splitOption); result.add(0, nextLink); break; } else { // Get a specific author Author author = listauthors.get(i); logger.debug("getAuthorEntry:" + author); Element entry = getDetailedEntry(breadcrumbs, author, mapOfBooksByAuthor.get(author)) ; if (entry != null) { result.add(entry); logger.debug("adding author to the TROOK database:" + author); TrookSpecificSearchDatabaseManager.addAuthor(author, entry); } } } } feed.addContent(result); Element entry; String urlInItsSubfolder = CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, pBreadcrumbs.size() >1 || pageNumber != 1); entry = createPaginateLinks (feed, filename, pageNumber, maxPages); createFilesFromElement(feed, filename, HtmlManager.FeedType.Catalog, true); if (from == 0) { entry = FeedHelper.getCatalogEntry(title, urn, urlInItsSubfolder, summary, // #751211: Use external icons option useExternalIcons ? getIconPrefix(inSubDir) + Icons.ICONFILE_AUTHORS : Icons.ICON_AUTHORS); } return entry; } /** * Get a list of author that needs to be split by letter * It might be necessary to recurse to further levels if this * is allowed by the maximum split level setting * * @param pBreadcrumbs The point we have currently reached * @param mapOfAuthorsByLetter The list of authors to list * @param baseTitle The base URL for this level * @param baseUrn The base Filename for this level * @param baseFilename The base filename form this level * @return The link to this page for the parent * @throws IOException */ private List<Element> getListOfAuthorsSplitByLetter( Breadcrumbs pBreadcrumbs, Map<String, List<Author>> mapOfAuthorsByLetter, String baseTitle, String baseUrn, String baseFilename) throws IOException { if (Helper.isNullOrEmpty(mapOfAuthorsByLetter)) return null; if (! baseFilename.startsWith(Constants.AUTHORLIST_TYPE)) { int dummy = 1; } boolean inSubDir = getCatalogLevel().length() > 0 || pBreadcrumbs.size() > 1; String sTitle = baseTitle; if (Helper.isNotNullOrEmpty(sTitle)) sTitle = sTitle + ", "; List<Element> result = new LinkedList<Element>(); SortedSet<String> letters = new TreeSet<String>(mapOfAuthorsByLetter.keySet()); Element element; for (String letter : letters) { // generate the letter file String letterFilename = Helper.getSplitString(baseFilename, letter, Constants.TYPE_SEPARATOR); String letterUrn = Helper.getSplitString(baseUrn,letter,Constants.URN_SEPARATOR); List<Author> authorsInThisLetter = mapOfAuthorsByLetter.get(letter); assert (authorsInThisLetter.size() > 0) : "No authors for letter sequence '" + letter + "'"; Collections.sort(authorsInThisLetter); String letterTitle; if (letter.equals(Constants.TYPE_SEPARATOR)) letterTitle = Localization.Main.getText("splitByLetter.author.other"); else letterTitle = Localization.Main.getText("splitByLetter.letter", Localization.Main.getText("authorword.title"), letter.length() > 1 ? letter.substring(0,1) + letter.substring(1).toLowerCase() : letter); // try and list the items to make the summary String summary = Summarizer.summarizeAuthors(authorsInThisLetter); /* * Prepare the list of authors in any case, even if it will be skipped by SplitByAuthorInitialGoToBooks. * It'll be useful in cross references */ logger.debug("calling getListOfBooks for the letter " + letter); element = getSubCatalog(pBreadcrumbs, authorsInThisLetter, true, 0, letterTitle, summary, letterUrn, letterFilename, checkSplitByLetter(letter)); assert element != null; if (element != null) { result.add(element); } if (currentProfile.getSplitByAuthorInitialGoToBooks()) { logger.debug("getting all books by all the authors in this letter"); List<Book> books = new LinkedList<Book>(); for (Author author : authorsInThisLetter) { books.addAll(mapOfBooksByAuthor.get(author)); } if (logger.isTraceEnabled()) logger.trace("getListOfAuthorsSplitByLetter: Breadcrumbs=" + pBreadcrumbs.toString()); element = getListOfBooks(pBreadcrumbs, books, true, 0, // Starting from start letterTitle, summary, letterUrn, letterFilename, SplitOption.DontSplit, // Bug #716917 Do not split on letter // #751211: Use external icons option useExternalIcons ? getIconPrefix(inSubDir) + Icons.ICONFILE_BOOKS : Icons.ICON_BOOKS, null); assert element != null; if (element != null) { result.add(element); } } } return result; } public List<Author> getAuthors() { return authors; } /** * Get the base filename that is used to store a given author * * Since we always hold a full list of authors at the top level the * name can be derived purely knowing the author involved. * @param author * @return */ public static String getAuthorFolderFilenameNoLevel(Author author) { return getCatalogBaseFolderFileNameIdNoLevelSplit(Constants.AUTHOR_TYPE,author.getId(), 1000); } /** * Get the base filename that is used to store a given author * This version works within the given level * @param author * @return */ public String getAuthorFolderFilenameWithLevel (Author author) { return getCatalogBaseFolderFileNameIdSplit(Constants.AUTHOR_TYPE, author.getId(), 1000); } /** * Get the details for the specified author * * @param pBreadcrumbs * @param author * @param opts authorbooks * @return */ // public Element getAuthorEntry(Breadcrumbs pBreadcrumbs, Author author, List<Book> authorbooks) throws IOException { public Element getDetailedEntry(Breadcrumbs pBreadcrumbs, Author author, Object... opts) throws IOException { assert opts[0] != null && opts[0].getClass().equals(LinkedList.class); List<Book> authorbooks = (List<Book>)opts[0]; if (logger.isDebugEnabled()) logger.debug(pBreadcrumbs + "/" + author); CatalogManager.callback.showMessage(pBreadcrumbs.toString()); if (!isInDeepLevel()) CatalogManager.callback.incStepProgressIndicatorPosition(); List listOfBooksInSeries = new LinkedList<Book>(); List listOfBooksNotInSeries = new LinkedList<Book>(); // We only need to worry about series if they are being listed under the author. if (currentProfile.getShowSeriesInAuthorCatalog()) { for (Book book : authorbooks) { Series serie = book.getSeries(); if (serie != null) { listOfBooksInSeries.add(book); } else { listOfBooksNotInSeries.add(book); } } } List<Element> firstElements = null; List<Book> morebooks = null; // sort by title logger.debug("sort 'booksByThisAuthor' by title"); BooksSubCatalog.sortBooksByTitle(authorbooks); if (Helper.isNullOrEmpty(author)) { return null; } String filename = getAuthorFolderFilenameWithLevel(author); logger.debug("getAuthorEntry:generating " + filename); String title = currentProfile.getDisplayAuthorSort() ? author.getSort(): author.getName(); String urn = Constants.INITIAL_URN_PREFIX + Constants.AUTHOR_TYPE + Constants.URN_SEPARATOR + author.getId(); Breadcrumbs breadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, CatalogManager.getCatalogFileUrl(filename + Constants.PAGE_ONE_XML, true)); // try and list the items to make the summary String summary = Summarizer.summarizeBooks(authorbooks); // We like to list series if we can before books not in series // (unless the user has suppressed series generation). if (listOfBooksInSeries.size() > 0 && currentProfile.getShowSeriesInAuthorCatalog()) { logger.debug("processing the series by " + author); // make a link to the series by this author catalog logger.debug("make a link to the series by this author catalog"); SeriesSubCatalog seriesSubCatalog = new SeriesSubCatalog(listOfBooksInSeries); seriesSubCatalog.setCatalogLevel(getCatalogLevel()); seriesSubCatalog.setCatalogFolderSplit(Constants.AUTHOR_TYPE, author.getId()); seriesSubCatalog.setCatalogBaseFilename(Constants.AUTHOR_TYPE + Constants.TYPE_SEPARATOR + author.getId() + Constants.TYPE_SEPARATOR + Constants.SERIES_TYPE); firstElements = seriesSubCatalog.getListOfSeries(breadcrumbs, null, // series derived from catalog books true, 0, // from start title, null, // summary not needed as only single series? urn, null, // filename derived from catalog properties SplitOption.Paginate, true); seriesSubCatalog = null; // May not be necessary, but allow earlier release of resources // Make a link to the "allbooks entry" for this author AllBooksSubCatalog allbooksSubcatalog = new AllBooksSubCatalog(authorbooks); AllBooksSubCatalog.sortBooksByTitle(authorbooks); allbooksSubcatalog.setCatalogLevel(getCatalogLevel()); allbooksSubcatalog.setCatalogFolder(filename); allbooksSubcatalog.setCatalogBaseFilename(filename + Constants.TYPE_SEPARATOR + Constants.ALLBOOKS_TYPE); Element entry = allbooksSubcatalog.getListOfBooks(breadcrumbs, allbooksSubcatalog.getBooks(), true, 0, // from start Localization.Main.getText("bookentry.author", Localization.Main.getText("allbooks.title"), author.getName()), allbooksSubcatalog.getSummary(), allbooksSubcatalog.getUrn(), allbooksSubcatalog.getCatalogBaseFolderFileName(), SplitOption.Paginate, useExternalIcons ? getIconPrefix(true) + Icons.ICONFILE_BOOKS : Icons.ICON_BOOKS, null); allbooksSubcatalog = null; // May not be necessary - but allowe earlier release of resources firstElements.add(0,entry); // Add at start (in front of Series list) // Reset books to list non-series books morebooks = listOfBooksNotInSeries; logger.debug("processing the other " + morebooks.size() + " books by " + author); } else { // No series (or we do not want them in author - simply take all te books to list them assert authorbooks != null; morebooks = authorbooks; logger.debug("there are no series by " + author + ", processing all his " + morebooks.size() + " books"); // try and list the items to make the summary logger.debug("try and list the items to make the summary"); summary = Summarizer.summarizeBooks(morebooks); } // sort 'morebooks' by title logger.debug("sort books by title"); sortBooksByTitle(morebooks); logger.debug("calling getListOfBooks with " + morebooks.size() + " books"); logger.trace("getAuthorEntry Breadcrumbs=" + pBreadcrumbs.toString()); author.setDone(); Element result = getListOfBooks(pBreadcrumbs, morebooks, true, // Always in subDir 0, // from title, summary, urn, filename, SplitOption.DontSplit, // Bug #716917 Do not split on letter // #751211: Use external icons option useExternalIcons ? getIconPrefix(true) + Icons.ICONFILE_AUTHORS : Icons.ICON_AUTHORS, firstElements); return result; } }