package com.gmail.dpierron.calibre.opds; /** * Class that provides the facilities for listing the books in a catalog * The type specific catalogs will extend this class to inherit its functionality * * Inherits from: * -> SubCatalog * Note that this is an abstract class so cannot be instantiated directly. */ import com.gmail.dpierron.calibre.cache.CachedFile; import com.gmail.dpierron.calibre.cache.CachedFileManager; import com.gmail.dpierron.calibre.configuration.DeviceMode; import com.gmail.dpierron.calibre.datamodel.*; import com.gmail.dpierron.tools.i18n.Localization; import com.gmail.dpierron.tools.i18n.LocalizationHelper; import com.gmail.dpierron.calibre.opds.indexer.IndexManager; 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.File; import java.io.IOException; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.MessageFormat; import java.util.*; public abstract class BooksSubCatalog extends SubCatalog { private final static Logger logger = LogManager.getLogger(BooksSubCatalog.class); /** * @return */ public boolean isBookTheStepUnit() { return false; } /** * Create a filtered books sub-catalog item * * @param stuffToFilterOut * @param books */ public BooksSubCatalog(List<Object> stuffToFilterOut, List<Book> books) { super(stuffToFilterOut, books); } /** * Create an un-filtered books sub-catalog item * * @param books */ public BooksSubCatalog(List<Book> books) { super(books); } public List<Book> getObjectList() { return getBooks(); } /** * Get a list of books starting from a specific point * * This function is the control routine and is called once * for each page at the same level, or each time a split occurs. * It is called recursively - and thus will be called once per file. * * ITIMPI: At the moment this function can call itself recursively with the 'from' * parameter being incremented. It is likely to be much more efficient * in both cpu load and memory usage to flatten the loop by rewriteing the * function to elimiate recursion. * * @param pBreadcrumbs * @param listbooks * @param inSubDir * @param from * @param title * @param summary * @param urn * @param pFilename * @param splitOption This option how a list should be split if it exceeds size limits * @param icon * @param firstElements Passed as null if not known * @param options * @return * @throws IOException */ public Element getListOfBooks(Breadcrumbs pBreadcrumbs, List<Book> listbooks, boolean inSubDir, int from, String title, String summary, String urn, String pFilename, SplitOption splitOption, String icon, List<Element> firstElements, Option... options) throws IOException { // return getListOfObjects(pBreadcrumbs, (List<GenericDataObject>)listbooks, inSubDir,from,title,summary,urn,pFilename,splitOption,icon,firstElements,options); if (logger.isDebugEnabled()) logger.debug("getListOfBooks: START"); // Special case of first time through when not all values set assert listbooks != null; // if (listbooks == null) listbooks = getBooks(); if (pFilename == null) pFilename = getCatalogBaseFolderFileName(); // Now some consistency checks // Now get on with main processing int catalogSize = listbooks.size(); if (logger.isDebugEnabled()) logger.debug("getListOfBooks:catalogSize=" + catalogSize); if (from != 0) inSubDir = true; if (Helper.isNotNullOrEmpty(pBreadcrumbs) && pBreadcrumbs.size() > 1) inSubDir = true; if (inSubDir && icon.startsWith(Constants.CURRENT_PATH_PREFIX)) icon = Constants.PARENT_PATH_PREFIX + icon.substring(2); // Work out any split options // Fixes #716917 when applied to author books list boolean willSplitByLetter; boolean willSplitByDate; if (splitOption == null) { // ITIMPI: Null seems to be equivalent to SplitByLetter ! // Might be better to replace calls by explicit value? splitOption = SplitOption.SplitByLetter; if (logger.isDebugEnabled()) logger.debug("getListOfBooks:splitOption=null. Changed to SplitByLetter"); } switch (splitOption) { case Paginate: if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=Paginate"); willSplitByLetter = false; willSplitByDate = false; break; case DontSplitNorPaginate: if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=DontSplitNorPaginate"); assert from == 0 : "getListBooks: DontSplitNorPaginate, from=" + from; willSplitByLetter = false; willSplitByDate = false; break; case DontSplit: // Bug #716917 Do not split on letter (used in Author and Series book lists) if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=DontSplit"); assert from == 0 : "getListBooks: DontSplit, from=" + from; willSplitByLetter = false; willSplitByDate = false; break; case SplitByDate: if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=SplitByDate"); assert from == 0 : "getListBooks: splitByDate, from=" + from; willSplitByLetter = checkSplitByLetter(splitOption, listbooks.size()); willSplitByDate = true; break; case SplitByLetter: if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=SplitByLetter"); assert from == 0 : "getListBooks: splitByLetter, from=" + from; willSplitByLetter = checkSplitByLetter(splitOption, listbooks.size()); willSplitByDate = false; break; default: // ITIMPI: Not sure that this case can ever arise // Just added as a safety check if (logger.isTraceEnabled()) logger.trace("getListOfBooks:splitOption=" + splitOption); assert from == 0 : "getListBooks: unknown splitOption, from=" + from; willSplitByLetter = checkSplitByLetter(splitOption, listbooks.size()); willSplitByDate = false; break; } // See if SplitByLetter conditions actually apply? if ((currentProfile.getBrowseByCover()) && (currentProfile.getBrowseByCoverWithoutSplit())) { willSplitByLetter = false; } if (logger.isTraceEnabled()) logger.trace("getListOfBooks:willSplitByLetter=" + willSplitByLetter); if (logger.isTraceEnabled()) logger.trace("getListOfBooks:willSplitByDate=" + willSplitByDate); if (logger.isTraceEnabled()) logger.trace("listing books from=" + from + ", title=" + title); int pageNumber = Summarizer.getPageNumber(from + 1); int maxPages = Summarizer.getPageNumber((willSplitByDate || willSplitByLetter) ? 0 : catalogSize); // generate the book list files String filename = pFilename + Constants.PAGE_DELIM + Integer.toString(pageNumber); String urlExt = CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, pBreadcrumbs.size() > 1 || inSubDir); Element feed; feed = FeedHelper.getFeedRootElement(pBreadcrumbs, title, urn, urlExt, true /*inSubDir */); // Update breadcrumbs // - If we are not on doing an author sub-catalog then add the author to the breadcrumbs // - If we are not on the first page then keep the existing breadcrumbs. Breadcrumbs breadcrumbs = pBreadcrumbs; // #c2o-204 breadrumbs should already be correct if listing first page of books for an author. if (from == 0 && ! (getCatalogFolder().startsWith(Constants.AUTHOR_TYPE) || getCatalogFolder().startsWith(Constants.AUTHORLIST_TYPE))) { breadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, urlExt); } // list the books (or split them) List<Element> result; if (willSplitByDate) { // Split by date listing result = getListOfBooksSplitByDate(breadcrumbs, DataModel.splitBooksByDate(listbooks), true /*inSubDir*/, // Musy be true if splitting by date title, urn, pFilename, icon, options); } else if (willSplitByLetter) { // Split by letter listing result = getListOfBooksSplitByLetter(breadcrumbs, DataModel.splitBooksByLetter(listbooks), true /*inSubDir*/, // Must be true if splitting by letter title, urn, pFilename, SplitOption.SplitByLetter, icon, options); } else { // Paginated listing result = new LinkedList<Element>(); String progressText = Breadcrumbs.getProgressText(breadcrumbs); progressText += " (" + Summarizer.getBookWord(listbooks.size()) + ")"; CatalogManager.callback.showMessage(progressText.toString()); for (int i = from; i < listbooks.size(); i++) { // check if we must continue CatalogManager.callback.checkIfContinueGenerating(); // See if we need to do the next page if ((splitOption != SplitOption.DontSplitNorPaginate) && ((i - from) >= maxBeforePaginate)) { // TODO #c2o-208 Add Previous, First and Last links if needed // ... YES - so go for next page if (logger.isDebugEnabled()) logger.debug("making a nextpage link"); Element nextLink = getListOfBooks(breadcrumbs, listbooks, true, // Awlays in SubDir (need to check this) i, // Continue nfrom where we were title, summary, urn, pFilename, splitOption != SplitOption.DontSplitNorPaginate ? SplitOption.Paginate : splitOption, icon, null, // No firstElements options); result.add(0, nextLink); break; } else { // ... NO - so add book to this page Book book = listbooks.get(i); if (logger.isTraceEnabled()) logger.trace("getListOfBooks: adding book to the list : " + book); try { logger.trace("getListOfBooks: breadcrumbs=" + breadcrumbs + ", book=" + book + ", options=" + options); Element entry = getDetailedEntry(breadcrumbs, book, options); if (entry != null) { if (logger.isTraceEnabled()) logger.trace("getListOfBooks: entry=" + entry); result.add(entry); TrookSpecificSearchDatabaseManager.addBook(book, entry); } } catch (RuntimeException e) { logger.error("getListOfBooks: Exception on book: " + book.getTitle() + "[" + book.getId() + "]", e); throw e; } } } } // if needed, add the first elements to the feed if (Helper.isNotNullOrEmpty(firstElements)) feed.addContent(firstElements); // add the book entries to the feed feed.addContent(result); Element entry; String urlInItsSubfolder = CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, inSubDir); entry = createPaginateLinks(feed, filename, pageNumber, maxPages); // TODO Following should take account of whether CoverMode changed createFilesFromElement(feed, filename, HtmlManager.FeedType.Catalog, CatalogManager.IsCoverFlowModeSame()); if (from == 0) { entry = FeedHelper.getCatalogEntry(title, urn, urlInItsSubfolder, summary, icon); } return entry; } /** * Get a list of books split by letter * It is invoked when a list of books is to be further sub-divided by letter. * * @param pBreadcrumbs * @param mapOfBooksByLetter * @param baseTitle * @param baseUrn * @param baseFilename * @param splitOption * @param icon * @param options * @return * @throws IOException */ private List<Element> getListOfBooksSplitByLetter( Breadcrumbs pBreadcrumbs, Map<String, List<Book>> mapOfBooksByLetter, boolean inSubDir, String baseTitle, String baseUrn, String baseFilename, SplitOption splitOption, String icon, Option... options) throws IOException { if (Helper.isNullOrEmpty(mapOfBooksByLetter)) return null; if (pBreadcrumbs.size() > 1) inSubDir = true; String sTitle = baseTitle; if (Helper.isNotNullOrEmpty(sTitle)) sTitle += ", "; List<Element> result = new LinkedList<Element>(); SortedSet<String> letters = new TreeSet<String>(mapOfBooksByLetter.keySet()); 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<Book> booksInThisLetter = mapOfBooksByLetter.get(letter); String letterTitle; if (letter.equals("_")) letterTitle = Localization.Main.getText("splitByLetter.book.other"); else letterTitle = Localization.Main.getText("splitByLetter.letter", Localization.Main.getText("bookword.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.summarizeBooks(booksInThisLetter); Element element = null; if (booksInThisLetter.size() > 0) { element = getListOfBooks(pBreadcrumbs, booksInThisLetter, true, // Always inSubDir if in letter 0, // start at first page letterTitle, summary, letterUrn, letterFilename, checkSplitByLetter(letter), icon, null, // No firstElements options); } else { // ITIMPI: Assert to check if the logic can ever let this be zero! assert (booksInThisLetter.size() <= 0) : "booksInThisLetter=" + booksInThisLetter.size() + " for letter '" + letter + "'"; } if (element != null) result.add(element); } return result; } /** * Get a list of books split by date * * These lists are used in the Recent Books catalog sub-section. * * @param pBreadcrumbs * @param mapOfBooksByDate * @param baseTitle * @param baseUrn * @param baseFilename * @param icon * @param options * @return * @throws IOException */ private List<Element> getListOfBooksSplitByDate( Breadcrumbs pBreadcrumbs, Map<DateRange, List<Book>> mapOfBooksByDate, boolean inSubDir, String baseTitle, String baseUrn, String baseFilename, String icon, Option... options) throws IOException { if (Helper.isNullOrEmpty(mapOfBooksByDate)) return null; String sTitle = baseTitle; if (Helper.isNotNullOrEmpty(sTitle)) sTitle = sTitle + ", "; if (pBreadcrumbs.size() > 1) inSubDir = true; List<Element> result = new LinkedList<Element>(); SortedSet<DateRange> ranges = new TreeSet<DateRange>(mapOfBooksByDate.keySet()); for (DateRange range : ranges) { // generate the range file String rangeFilename = baseFilename + Constants.TYPE_SEPARATOR + range; String rangeUrn = Helper.getSplitString(baseUrn, range.toString(), Constants.URN_SEPARATOR); String rangeTitle = LocalizationHelper.getEnumConstantHumanName(range); // For the "Today" category we wabt the actual date to which this applies. if (range == DateRange.ONEDAY) { // rangeTitle += " (" + DateFormat.getDateInstance(DateFormat.DEFAULT, currentProfile.getLanguage()).format(new Date()) + ")"; rangeTitle = DateFormat.getDateInstance(DateFormat.DEFAULT, currentProfile.getLanguage()).format(new Date()); } List<Book> booksInThisRange = mapOfBooksByDate.get(range); // try and list the items to make the summary String summary = Summarizer.summarizeBooks(booksInThisRange); Element element = null; // We Always generate the "Today" entry so we can see // what the other ranges are relative to. if (booksInThisRange.size() > 0) { element = getListOfBooks(pBreadcrumbs, booksInThisRange, true, // Always inSubDir 0, // Start at first page rangeTitle, summary, rangeUrn, rangeFilename, SplitOption.Paginate, icon, null, options); } if (element != null) result.add(element); } // end of for return result; } // ---------------- // BOOK ENTRY // ---------------- // The remainder of the methods are specific to creating an entry for a specific book /** * Add the aquisition links * * These are used to specify where a book can be downloaded from. * They will not be needed if generation of download links is suppressed. * * @param book * @param entry */ private void addAcquisitionLinks(Book book, Element entry) { if (!currentProfile.getGenerateOpdsDownloads()) { if (logger.isTraceEnabled()) logger.trace("addAcquisitionLinks: exit: download links suppressed"); return; } // links to the ebook files if (logger.isTraceEnabled()) logger.trace("addAcquisitionLinks: links to the ebook files"); for (EBookFile file : book.getFiles()) { // prepare to copy the ebook file if (logger.isTraceEnabled()) logger.trace("addAcquisitionLinks: prepare to copy the ebook file " + file.getName()); // TODO ITIMPI Why is EPUB treated as a special case? CatalogManager.addFileToTheMapOfLibraryFilesToCopy(file.getFile(), (file.getFormat() == EBookFormat.EPUB) ? book : null); // Allow for books on specific URL (#c2o-160) String prefix = currentProfile.getUrlBooks(); if (Helper.isNullOrEmpty(prefix)) { prefix = Constants.PARENT_PATH_PREFIX + Constants.PARENT_PATH_PREFIX ; } entry.addContent(FeedHelper.getAcquisitionLink(prefix + FeedHelper.urlEncode(book.getPath(), true) + Constants.FOLDER_SEPARATOR + FeedHelper.urlEncode(file.getName() + file.getExtension(), true), file.getFormat().getMime(), // Mime type Localization.Main.getText("bookentry.download", file.getFormat()), currentProfile.getIncludeSizeOfDownloads() ? org.apache.commons.io.FileUtils.byteCountToDisplaySize(file.getFile().length()):"")); // if the IncludeOnlyOneFile option is set, break to avoid publishing other files if (currentProfile.getIncludeOnlyOneFile()) { if (logger.isTraceEnabled()) logger.trace("addAcquisitionLinks: break to avoid publishing other files"); break; } } } /** * Add an image link (cover or thumbnail) * * If necessary we will generate a resized image at this point. * * If we are including the covers in the catalog we want to * embed the image as base64 data to reduce the number of files. * * If there is no valibre cover image then we use our own default image, * and in this case do not bother with resizing it. * * @param book // The book to which this image applies * @param entry // The entry to which the image URL should be added * @param iManager // The particular ImageManager object * @param useResizeImage // true if we need a resized image. false implies use existing cover image * @param isCover // true if a cover, false if it is a thumbnail */ private void addImageLink (Book book, Element entry, ImageManager iManager, boolean useResizeImage, boolean isCover) { File bookFolder = book.getBookFolder(); // File containg cover image. // We initally assume there is a Calibre cover file in the library. CachedFile coverFile = CachedFileManager.addCachedFile(bookFolder, Constants.CALIBRE_COVER_FILENAME); // CachedFile defaultCoverFile = null; // Filename (without path) of the image to be resized String imageFilename; // The file that contains the image to be used CachedFile imageFile; // Filename of the resized image file (without path) String resizedImageFilename = iManager.getResizedFilename(); // File to be used when resized images in use CachedFile resizedImageFile = CachedFileManager.addCachedFile(bookFolder, resizedImageFilename); // The URI to the image at runtime String imageUri = null; // Name when stored in catalog String catalogImageFilename; // We normally expect a calibre cover file to exist as Calibre // will normally have inserted its default ocver. // If by any chance it does not exist we simply always use our default image // In such a case we never resize it as the default is small. if (! coverFile.exists()) { // **** WE Do NOT HAVE A CALIBRE COVER IMAGE **** // We only want the warning once per book if (book.isDone() == false) { if (isCover) logger.warn("addImageFile: No cover file found forbook " + book); // We remove any resized image created by earlier versions of calibre2opds if (resizedImageFile.exists()) { if (logger.isTraceEnabled()) logger.trace("addImageFile: Cover missing, so removing " + resizedImageFile); resizedImageFile.delete(); CachedFileManager.removeCachedFile(resizedImageFile);; } } // We will use our default cover instead! coverFile = defaultCoverFile; // We never resize the default image file! imageFile = coverFile; imageUri = defaultCoverUri; catalogImageFilename = Constants.PARENT_PATH_PREFIX + Constants.DEFAULT_IMAGE_FILENAME; } else { // **** WE DO HAVE A CALIBRE COVER IMAGE **** // Handle migration to new name standard (#c2o_???) // This removes images created by earlier calibre2opds releases. FeedHelper.checkFileNameIsNewStandard(resizedImageFile, new File(bookFolder, iManager.getResizedFilenameOld(book))); if (useResizeImage) { // We DO want to use a resized image imageUri = // #c2o_223 Need to use image from Books URI if it is specified (Helper.isNullOrEmpty(booksURI) ? FeedHelper.urlEncode(Constants.LIBRARY_PATH_PREFIX, true) : booksURI) + FeedHelper.urlEncode( book.getPath() + Constants.FOLDER_SEPARATOR + iManager.getResizedFilename(), true); imageFile = resizedImageFile; catalogImageFilename = getBookFolderFilename(book) + Constants.TYPE_SEPARATOR + iManager.getResizedFilename(); // #c2o-238 Only create image if we have not already done this book previously if (book.isDone()) { if (logger.isTraceEnabled()) logger.trace("addImageLink: skipping creating image - book already done previously"); } else { // We ned to generate if it is missing, the size has changed or the cover file is newer than te resized file. if (!iManager.hasImageSizeChanged() && resizedImageFile.exists() && (resizedImageFile.lastModified() > coverFile.lastModified())) { if (logger.isTraceEnabled()) logger.trace("addImageLink: resizedCover exissts - not to be regenerated"); } else { if (logger.isTraceEnabled()) { if (!resizedImageFile.exists()) { logger .trace("addImageLink: resizedImage set to be generated (not already existing)"); } else if (CatalogManager.coverManager.hasImageSizeChanged()) { logger.trace("addImageLink: resizedImage set to be generated (image size changed)"); } else if (resizedImageFile.lastModified() < coverFile.lastModified()) { logger.trace("addImageLink: resizedImage set to be generated (new cover)"); } } iManager .generateImage(resizedImageFile, coverFile.exists() ? coverFile : defaultCoverFile); } } } else { // Not using resized covers - // Delete any resized image file if (resizedImageFile.exists()) { if (logger.isTraceEnabled()) logger.trace("addImageLink: deleted unwanted resized image " + resizedImageFile); resizedImageFile.delete(); } imageUri = // #c2o_223 Need to use image from Books URI if it is specified (Helper.isNullOrEmpty(booksURI) ? FeedHelper.urlEncode(Constants.LIBRARY_PATH_PREFIX, true) : booksURI) + FeedHelper.urlEncode( book.getPath() + Constants.FOLDER_SEPARATOR + Constants.CALIBRE_COVER_FILENAME, true); imageFile = coverFile; catalogImageFilename = getBookFolderFilename(book) + Constants.TYPE_SEPARATOR + Constants.CALIBRE_COVER_FILENAME; } } // If we are generating a catalog for a Nook we cache the results for use later if (iManager.equals(CatalogManager.thumbnailManager) && currentProfile.getGenerateIndex()) { CatalogManager.thumbnailManager.addBook(book, imageUri); } // Are we ebedding images in the catalog XML/HTML or using external ones? if (! useExternalImages) { // Embed the image into the XML/HTM as base 64 data imageUri = iManager.getFileToBase64Uri(imageFile); } else { // If not in default mode we need to copy them to the published area if (! currentProfile.getDeviceMode().equals(DeviceMode.Default)) { CatalogManager.addFileToTheMapOfFilesToCopy(imageFile); } } // Are we storing images in the catalog? if (includeCoversInCatalog) { if (coverFile.exists()) { if (! useExternalImages && ! catalogImageFilename.equals(Constants.PARENT_PATH_PREFIX + iManager.getDefaultResizedFilename())) { imageUri = iManager.getFileToBase64Uri(imageFile); } else { CatalogManager.addImageFileToTheMapOfCatalogImages(catalogImageFilename, imageFile); if (isCover) { imageUri=catalogImageFilename.substring(catalogImageFilename.indexOf(Constants.FOLDER_SEPARATOR)+1); } else { imageUri=Constants.PARENT_PATH_PREFIX + catalogImageFilename; } } } } entry.addContent(FeedHelper.getImageLink(imageUri,isCover)); } /** * Add book cross reference links * * Used when constructing book details entries * * NOTE: At the moment we do not constrict these to the current level * We might want to revisit this assumption? * * @param entry * @param book */ private void addNavigationLinks(Element entry, Book book) { String filename; if ( ! currentProfile.getGenerateCrossLinks()) { return; } // add the series link // (but only if we generate a series catalog) if (isSeriesCrossreferences(book)) { Series serie = book.getSeries(); if (logger.isTraceEnabled()) logger.trace("addNavigationLinks: add the series link"); // Series for cross-references are always held at top level // TODO Perhaps consider whether level should be taken into account? filename = SeriesSubCatalog.getSeriesFolderFilenameNoLevel(serie) + Constants.PAGE_ONE_XML; entry.addContent(FeedHelper.getRelatedLink(CatalogManager.getCatalogFileUrl(filename, true), book.getSerieIndex() == 0 ? Localization.Main.getText("bookentry.seriesonly", serie.getName()) : Localization.Main.getText("bookentry.seriesindex", book.getSerieIndex(), serie.getName()))); serie.setReferenced(); } String booksText = Localization.Main.getText("bookword.title"); // add the author page link(s) // (but only if we generate an authors catalog) if (isAuthorCrossReferences(book)) { if (logger.isTraceEnabled()) logger.trace("addNavigationLinks: add the author page link(s)"); for (Author author : book.getAuthors()) { String authorName = author.getName(); // Check for author names that do not get internal links. if (authorName.toUpperCase().equals("UNKNOWN") || authorName.toUpperCase().equals("VARIOUS")) { continue; } // c2o-168 - Omit Counts if MinimizeChangedFiles set // TODO: Check this out now minimizeChangedFiles setting emoved // TODO: Initial thinking is include counts again. /* if (! currentProfile.getMinimizeChangedFiles()) { booksText = Summarizer.getBookWord(DataModel.getMapOfBooksByAuthor().get(author).size()); } */ // Authors for cross-references are always held at top level ! // TODO Perhaps consider whether level should be taken into account? filename = AuthorsSubCatalog.getAuthorFolderFilenameNoLevel(author) + Constants.PAGE_ONE_XML; entry.addContent(FeedHelper.getRelatedLink(CatalogManager.getCatalogFileUrl(filename, true), Localization.Main.getText("bookentry.author", booksText, authorName))); author.setReferenced(); } } // add the tags links // (but only if we generate a tags catalog) // TODO: Shpi;d we do something different if the option to split tags // TODO is enabled - e.g. go to each section individully? if (isTagCrossReferences(book)) { if (logger.isTraceEnabled()) logger.trace("addNavigationLinks: add the tags links"); for (final Tag tag : book.getTags()) { if (! CatalogManager.getTagsToIgnore().contains(tag)) { // #c2o_192 int nbBooks = DataModel.getMapOfBooksByTag().get(tag).size(); // Tags for cross-references are held at top level // TODO Perhaps consider whether level should be taken into account? filename = TagsSubCatalog.getTagFolderFilenameNoLevel(tag) + Constants.PAGE_ONE_XML; if (nbBooks > 1) { // c2o-168 - Omit Counts if MinimizeChangedFiles set // TODO: Rethink this in light of minimizechangedfiles being removed // TODO: Probably best to act as if it had been specified? // if (! currentProfile.getMinimizeChangedFiles()) { // booksText = Summarizer.getBookWord(nbBooks); // } entry.addContent(FeedHelper.getRelatedLink(CatalogManager.getCatalogFileUrl(filename, true), Localization.Main.getText("bookentry.tags", booksText, tag.getName()))); tag.setReferenced(); } } } } // add the ratings links if (isRatingCrossReferences(book)) { if (logger.isTraceEnabled()) logger.trace("addNavigationLinks: add the ratings links"); int nbBooks = DataModel.getMapOfBooksByRating().get(book.getRating()).size(); if (nbBooks > 1) { BookRating rating = book.getRating(); // c2o-168 - Omit Counts if MinimizeChangedFiles set // TODO: Revisit assumption since minimizeChangedFiles option removed // TODO: Probably best to act as if it were set? // if (! currentProfile.getMinimizeChangedFiles()) { // booksText = Summarizer.getBookWord(nbBooks); // } // Ratings are held at level filename = getCatalogBaseFolderFileNameId(Constants.RATED_TYPE, rating.getId().toString()) + Constants.PAGE_ONE_XML; entry.addContent(FeedHelper.getRelatedLink(CatalogManager.getCatalogFileUrl(filename, true), Localization.Main.getText("bookentry.ratings", booksText, LocalizationHelper.getEnumConstantHumanName(rating)))); rating.setReferenced(); } } } /** * Helper element for external links. * Controls whether links open in new window * * @param entry * @param url */ private void addExternalLinksHelper(Element entry, String url, String title) { Element urlElement; urlElement = FeedHelper.getRelatedHtmlLink(url, title); if (currentProfile.getNewWindowForExternalReferences()) { urlElement.setAttribute("target", "_blank"); } entry.addContent(urlElement); } /** * Add links for further information about a book * * Used when constructing the Book Details pages * * @param entry * @param book */ private void addExternalLinks(Element entry, Book book) { if (! currentProfile.getGenerateExternalReferences()) { return; } if (logger.isTraceEnabled()) { logger.trace("addExternalLinks: ADDING external links to book " + book); if (currentProfile.getNewWindowForExternalReferences()) logger.trace("addExternalLinks: (using new browser windows for the links)") ; } String url; String title; // ADD BOOK related links // add the GoodReads book link // We preferentially use the option to add via ISBN if we can if (Helper.isNotNullOrEmpty(book.getIsbn()) && (Helper.isNotNullOrEmpty(currentProfile.getGoodreadIsbnUrl()) || Helper.isNotNullOrEmpty(currentProfile.getGoodreadReviewIsbnUrl()))) { title=Localization.Main.getText("bookentry.goodreads"); url = currentProfile.getGoodreadIsbnUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: add the GoodReads ISBN book link"); url = MessageFormat.format(url, book.getIsbn()); addExternalLinksHelper(entry, url, title); } url = currentProfile.getGoodreadReviewIsbnUrl(); if (Helper.isNotNullOrEmpty(url)) if (logger.isTraceEnabled()) logger.trace("addExternalLinks: add the GoodReads Review ISBN book link"); title = Localization.Main.getText("bookentry.goodreads.review"); url= MessageFormat.format(url, book.getIsbn()); addExternalLinksHelper(entry, url, title); } else { url = currentProfile.getGoodreadTitleUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: add the GoodReads Title book link"); title = Localization.Main.getText("bookentry.goodreads"); url = MessageFormat.format(url, FeedHelper.urlEncode(book.getTitle())); addExternalLinksHelper(entry, url, title); } } // add the Wikipedia book link url = currentProfile.getWikipediaUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: add the Wikipedia book link"); title = Localization.Main.getText("bookentry.wikipedia"); url = MessageFormat.format(url, currentProfile.getWikipediaLanguage(), FeedHelper.urlEncode(book.getTitle())); addExternalLinksHelper(entry, url, title); } // Add Librarything book link // We use ISBN if possible, otherwise title+author title=Localization.Main.getText("bookentry.librarything"); url = currentProfile.getLibrarythingIsbnUrl(); if (Helper.isNotNullOrEmpty(book.getIsbn()) && Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: Add Librarything ISBN book link"); url = MessageFormat.format(url, book.getIsbn()); addExternalLinksHelper(entry, url, title); } else { url =currentProfile.getLibrarythingTitleUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: Add Librarything TITLE+Author book link"); url = MessageFormat.format(url, FeedHelper.urlEncode(book.getTitle()), FeedHelper.urlEncode(book.getMainAuthor().getName())); addExternalLinksHelper(entry, url, title); } } // Add Amazon book link // We use ISBN if possible, otherwise title+author url = currentProfile.getAmazonIsbnUrl(); title =Localization.Main.getText("bookentry.amazon"); if (Helper.isNotNullOrEmpty(book.getIsbn()) && Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: Add Amazon ISBN book link"); url = MessageFormat.format(url, book.getIsbn()); addExternalLinksHelper(entry, url, title); } else { url = currentProfile.getAmazonTitleUrl(); if (Helper.isNotNullOrEmpty(url) && (book.getMainAuthor() != null && Helper.isNotNullOrEmpty(book.getTitle()))) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: Add Amazon TITLE=author book link"); url = MessageFormat.format(url, FeedHelper.urlEncode(book.getTitle()), FeedHelper.urlEncode(book.getMainAuthor().getName())); addExternalLinksHelper(entry, url, title); } } // ADD AUTHOR related Links // Do for each author in turn if (currentProfile.getIncludeAuthorInBookDetails() && book.hasAuthor()) { for (Author author : book.getAuthors()) { // add the GoodReads author link url = currentProfile.getGoodreadAuthorUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinksy: add the GoodReads author link for " + author.getName()); title = Localization.Main.getText("bookentry.goodreads.author", author.getName()); url = MessageFormat.format(url, FeedHelper.urlEncode(author.getName())); addExternalLinksHelper(entry, url, title); } // add the Wikipedia author link url = currentProfile.getWikipediaUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: add the Wikipedia author link for " + author.getName()); title = Localization.Main.getText("bookentry.wikipedia.author", author.getName()); url = MessageFormat.format(url, currentProfile.getWikipediaLanguage(), FeedHelper.urlEncode(author.getName())); addExternalLinksHelper(entry, url, title); } // add the LibraryThing author link url = currentProfile.getLibrarythingAuthorUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: add the LibraryThing author link for " + author.getName()); title = Localization.Main.getText("bookentry.librarything.author", author.getName()); // LibraryThing is very peculiar on how it looks up it's authors... format is LastNameFirstName[Middle] url= MessageFormat.format(url,FeedHelper.urlEncode(author.getSort().replace(",", "").replace(" ", ""))); addExternalLinksHelper(entry, url, title); } // add the Amazon author link url = currentProfile.getAmazonAuthorUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: add the Amazon author link for " + author.getName()); title = Localization.Main.getText("bookentry.amazon.author", author.getName()); url = MessageFormat.format(url, FeedHelper.urlEncode(author.getName())); addExternalLinksHelper(entry,url,title); } // add the ISFDB author link url = currentProfile.getIsfdbAuthorUrl(); if (Helper.isNotNullOrEmpty(url)) { if (logger.isTraceEnabled()) logger.trace("addExternalLinks: add the ISFDB author link for " + author.getName()); title = Localization.Main.getText("bookentry.isfdb.author", author.getName()); url = MessageFormat.format(url, FeedHelper.urlEncode(author.getName())); addExternalLinksHelper(entry, url, title); } } // End of Author loop } // End of Author section } /** * Generate a book entry in a catalog * * The amount of detail added depends on whether we are generating * a partial book entry (for a list of books) or a full entry (for book details) * * We use a common function as some of the data must be the same in both * the full and partial entries for a book. * * @param entry * @param book * @param isFullEntry */ private void decorateBookEntry(Element entry, Book book, boolean isFullEntry) { if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: ADDING book decoration to book " + book); ImageManager iManager; // cover and thumbnail links if (isFullEntry) { // We only need a cover image for full entries if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: ADDING cover link"); boolean resizeCover; if (currentProfile.getUseThumbnailsAsCovers()) { iManager = CatalogManager.thumbnailManager; resizeCover = currentProfile.getThumbnailGenerate(); } else { iManager = CatalogManager.coverManager; resizeCover = currentProfile.getCoverResize(); } addImageLink(book, entry, iManager,resizeCover,true); } // We want a thumbnail for both full and partial entries. if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: ADDING thumbnail link"); iManager = CatalogManager.thumbnailManager; addImageLink(book, entry, iManager, currentProfile.getThumbnailGenerate(), false); // acquisition links addAcquisitionLinks(book, entry); if (currentProfile.getIncludeAuthorInBookDetails()) { if (book.hasAuthor()) { for (Author author : book.getAuthors()) { if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: author " + author); // #c2o-190 String name = currentProfile.getDisplayAuthorSort() ? author.getSort() : author.getName(); Element authorElement = JDOMManager.element(Constants.OPDS_ELEMENT_AUTHOR) .addContent(JDOMManager.element(Constants.OPDS_ELEMENT_NAME).addContent(name)) .addContent(JDOMManager.element(Constants.OPDS_ELEMENT_URI).addContent(Constants.PARENT_PATH_PREFIX + AuthorsSubCatalog.getAuthorFolderFilenameNoLevel(author) + Constants.PAGE_ONE_XML)); entry.addContent(authorElement); } } } // published element if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: published " + book.getPublicationDate()); Element published = FeedHelper.getPublishedTag(book.getPublicationDate()); entry.addContent(published); // dublin core - language for (Language language : book.getBookLanguages()) { if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: language " + language.getIso2()); Element dcLang = FeedHelper.getDublinCoreLanguageElement(language.getIso2()); entry.addContent(dcLang); } // dublin core - publisher Publisher publisher = book.getPublisher(); if (Helper.isNotNullOrEmpty(publisher)) { if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: publisher " + publisher.getName()); Element dcPublisher = FeedHelper.getDublinCorePublisherElement(publisher.getName()); entry.addContent(dcPublisher); } // categories if (Helper.isNotNullOrEmpty(book.getTags())) { // tags for (Tag tag : book.getTags()) { if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: tag " + tag.getName()); Element categoryElement = FeedHelper.getCategoryElement(tag.getName()); entry.addContent(categoryElement); } } // series if (currentProfile.getIncludeSeriesInBookDetails() && Helper.isNotNullOrEmpty(book.getSeries())) { String seriesName = currentProfile.getSortSeriesUsingLibrarySort() ? book.getSeries().getName() : book.getSeries().getSort(); if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: series " + seriesName + "[" + book.getSerieIndex() + "]"); Element categoryElement = FeedHelper.getCategoryElement(seriesName); entry.addContent(categoryElement); } // book description if (isFullEntry) { if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: FULL ENTRY"); // content element if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: content element"); Element content = JDOMManager.element("content").setAttribute("type", "text/html"); boolean hasContent = false; if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: computing comments"); // Series (if present and wanted) if (currentProfile.getIncludeSeriesInBookDetails() && Helper.isNotNullOrEmpty(book.getSeries())) { String data = Localization.Main.getText("content.series.data", book.getSerieIndex(), currentProfile.getSortSeriesUsingLibrarySort() ? book.getSeries().getName() : book.getSeries().getSort()); content.addContent(JDOMManager.element(Constants.HTML_ELEMENT_PARAGRAPH) .addContent(JDOMManager.element(Constants.HTML_ELEMENT_STRONG) .addContent(Localization.Main.getText("content.series") + ": ")) .addContent(data)) ; hasContent = true; } // Rating (if present and wanted) // If the user has requested tags we output this section even if the list is empty. // The assumption is that the user in this case wants to see that no tags have been assigned // If we get feedback that this is not a valid addumption then we could omit it when the list is empty if (currentProfile.getIncludeRatingInBookDetails()) { if (Helper.isNotNullOrEmpty(book.getRating())) { String rating = LocalizationHelper.getEnumConstantHumanName(book.getRating()); content.addContent(JDOMManager.element(Constants.HTML_ELEMENT_PARAGRAPH) .addContent(JDOMManager.element(Constants.HTML_ELEMENT_STRONG) .addContent(Localization.Main.getText("content.rating") + ": ")) .addContent(rating) ); hasContent = true; } } // Tags (if present and wanted) // If the user has requested tags we output this section even if the list is empty. // The assumption is that the user in this case wants to see that no tags have been assigned // If we get feedback that this is not a valid addumption then we could omit it when the list is empty if (currentProfile.getIncludeTagsInBookDetails()) { if (Helper.isNotNullOrEmpty(book.getTags())) { String tags = book.getTags().toString(); if (tags != null && tags.startsWith("[")) // Remove braces added by java around a list tags = tags.substring(1, tags.length()-1); else // If no tags then we need an empty string (is this possible) tags = ""; content.addContent(JDOMManager.element(Constants.HTML_ELEMENT_PARAGRAPH) .addContent(JDOMManager.element(Constants.HTML_ELEMENT_STRONG) .addContent(Localization.Main.getText("content.tags") + ": ")) .addContent(tags) ); hasContent = true; } } // Publisher (if present and wanted) if (currentProfile.getIncludePublisherInBookDetails()) { if (Helper.isNotNullOrEmpty(book.getPublisher())) { content.addContent(JDOMManager.element(Constants.HTML_ELEMENT_PARAGRAPH) .addContent(JDOMManager.element(Constants.HTML_ELEMENT_STRONG) .addContent(Localization.Main.getText("content.publisher") + ": ")) .addContent(book.getPublisher().getName()) ); hasContent = true; } } // Published date (if present and wanted) if (currentProfile.getIncludePublishedInBookDetails()) { Date pubtmp = book.getPublicationDate(); if (Helper.isNotNullOrEmpty(pubtmp)) { content.addContent(JDOMManager.element(Constants.HTML_ELEMENT_PARAGRAPH) .addContent(JDOMManager.element(Constants.HTML_ELEMENT_STRONG) .addContent(Localization.Main.getText("content.published") + ": ")) .addContent(CatalogManager.bookDateFormat.format(book.getPublicationDate())) ); hasContent = true; } } // Added date (if present and wanted) if (currentProfile.getIncludeAddedInBookDetails()) { Date addtmp = book.getTimestamp(); if (Helper.isNotNullOrEmpty(addtmp)) { content.addContent(JDOMManager.element(Constants.HTML_ELEMENT_PARAGRAPH) .addContent(JDOMManager.element(Constants.HTML_ELEMENT_STRONG).addContent(Localization.Main.getText("content.added") + ": ")) .addContent(CatalogManager.titleDateFormat.format(addtmp))); hasContent = true; } } // Modified date (if present and wanted) if (currentProfile.getIncludeModifiedInBookDetails()) { Date modtmp = book.getModified(); if (Helper.isNotNullOrEmpty(modtmp)) { content.addContent(JDOMManager.element(Constants.HTML_ELEMENT_PARAGRAPH) .addContent(JDOMManager.element(Constants.HTML_ELEMENT_STRONG).addContent(Localization.Main.getText("content.modified") + ": ")) .addContent(CatalogManager.titleDateFormat.format(modtmp))); hasContent = true; } } // See if any Custom Column values to be included List<CustomColumnType>bookDetailsCustomColumnTypes = CatalogManager.getBookDetailsCustomColumns(); if (bookDetailsCustomColumnTypes != null && bookDetailsCustomColumnTypes.size() > 0) { List<CustomColumnValue> values = DataModel.getMapOfCustomColumnValuesByBookId().get(book.getId().toString()); for (CustomColumnType columnType : bookDetailsCustomColumnTypes) { String textValue = ""; String dataType = columnType.getDatatype(); String name = columnType.getName(); String label = columnType.getLabel(); if (values != null && values.size() > 0) { for (CustomColumnValue value : values) { if (value.getType().equals(columnType)) { textValue = value.getValueAsString(); break; } } } // If we have a value for a custom field then add it // (or always add it even when empty if the settings say so) if (currentProfile.getBookDetailsCustomFieldsAlways() || Helper.isNotNullOrEmpty(textValue)) { // Special processing for bool type // convert to localized yes/no text if (dataType.equals("bool")) { if (Helper.isNotNullOrEmpty(textValue)) { textValue = textValue.equals("0") ? Constants.NO : Constants.YES; } } // TODO: Special processing for Series type fields if (dataType.equals("series")) { } // Special processing for fields that look like links // We convert them to a link, and use name as the description. if (textValue.toUpperCase().startsWith("http://") || textValue.toString().startsWith("HTTPS://")) { name = "<u><a href=\"" + textValue + "\">" + name + "</a></u>"; textValue = ""; } else { name += ": "; } // Special processing for text fields that contain HTML (e.g. Calibre comment) // We want to remove the leading <DIV> tag inserted by Calibre String textvaluelower = textValue.toLowerCase(); int posStart = textvaluelower.startsWith("<div>") ? 5 : 0; if (posStart != 0) { int posEnd = textvaluelower.endsWith("</div>") ? textValue.length() - 6 : textValue.length(); // int posPara = textvaluelower.indexOf("<p>"); // We want a <DIV> around the custom field inserted by Calibre to be // changed to a <SPAN> to avoid unecessary white space being inserted at display time // if (posPara != -1) { // textValue = "<span id=\"" + label + "\">" + textValue.substring(posStart, posPara) + "</span>" + textValue.substring(posPara + 4); // } else { textValue = "<span id=\"" + label + "\">" + textValue.substring(posStart, posEnd) + "</span>"; // } } // Finally add results Element customElement = JDOMManager.element((Constants.HTML_ELEMENT_PARAGRAPH)); // Most of the time the name will be pure text, but in the // special case of it being an link it will be HTML Element nameElement = JDOMManager.element(Constants.HTML_ELEMENT_STRONG); if (name.startsWith("<")) { for (Element p : JDOMManager.convertHtmlTextToXhtml(name)) { nameElement.addContent(p.detach()); } } else { nameElement.addContent(name); } customElement.addContent(nameElement); // The text part can be either pur text or HTML with equal liklihood if (textvaluelower.startsWith("<")) { for (Element p : JDOMManager.convertHtmlTextToXhtml(textValue)) { customElement.addContent(p.detach()); } } else { customElement.addContent(textValue); } content.addContent(customElement); hasContent = true; } } } String commentsString = book.getComment(); List<Element> comments = JDOMManager.convertHtmlTextToXhtml(commentsString); if (Helper.isNotNullOrEmpty(comments)) { if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: got comments"); content.addContent(JDOMManager.element(Constants.HTML_ELEMENT_PARAGRAPH) .addContent(JDOMManager.element(Constants.HTML_ELEMENT_STRONG) .addContent(Localization.Main.getText("content.summary")))); for (Element p : comments) { content.addContent(p.detach()); } hasContent = true; } else { if (Helper.isNotNullOrEmpty(book.getComment())) { logger.warn(Localization.Main.getText("warn.badComment", book.getId() , book.getTitle())); logger.warn(book.getComment()); book.setComment(""); } } if (hasContent) { if (logger.isTraceEnabled()) logger.trace("decorateBookEntry: had content"); entry.addContent(content); } } else { // summary element (the shortened book comment) if (logger.isTraceEnabled()) logger.trace("getBookEntry: short comment"); String summary = book.getSummary(currentProfile.getMaxBookSummaryLength()); // If we had anything for the summary then it needs to be added to the output. if (Helper.isNotNullOrEmpty(summary)) { entry.addContent(JDOMManager.element("summary").addContent(summary)); } } if (isFullEntry) { // navigation links addNavigationLinks(entry, book); // external links addExternalLinks(entry, book); } } /** * Get the base filename that is used to store a given book * * Since we always hold books at the top level the name can be * derived purely knowing the book involved. * * @param book * @return */ public static String getBookFolderFilename(Book book) { return getCatalogBaseFolderFileNameIdNoLevelSplit(Constants.BOOK_TYPE,book.getId(),1000); } /** * Control generating a book Full Details entry * * The partial details are always generated as these are * required by the catalog entry that points to the book. * * The full details are only generated if it does not appear * that we have done these previosuly. * * @param pBreadcrumbs * @param bookObject * @param options * @return * @throws java.io.IOException */ // public Element getBookEntry(Breadcrumbs pBreadcrumbs, public Element getDetailedEntry(Breadcrumbs pBreadcrumbs, Object bookObject, Option... options) throws IOException { assert bookObject.getClass().equals(Book.class); Book book = (Book)bookObject; if (logger.isDebugEnabled()) logger.debug("getBookEntry: pBreadcrumbs=" + pBreadcrumbs + ", book=" + book); // Book files are always a top level (we might revisit this assumption one day) String filename = getBookFolderFilename(book); String fullEntryUrl = CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, true); File outputFile = CatalogManager.storeCatalogFile(filename + Constants.XML_EXTENSION); if (!isInDeepLevel() && isBookTheStepUnit() && !getCatalogFolder().startsWith(Constants.AUTHOR_TYPE)) CatalogManager.callback.incStepProgressIndicatorPosition(); if (logger.isDebugEnabled()) logger.debug("getBookEntry:" + book); if (logger.isTraceEnabled()) logger.trace("getBookEntry: pBreadcrumbs " + pBreadcrumbs.toString()); if (logger.isTraceEnabled()) logger.trace("getBookEntry: generating " + filename); // construct the contextual title (including the date, or the series, or the rating) // #c2o_190 String title = currentProfile.getDisplayTitleSort() ? book.getTitle_Sort() : book.getTitle(); if (Option.contains(options, Option.INCLUDE_SERIE_NUMBER)) { if (book.getSerieIndex() != 0) { DecimalFormat df = new DecimalFormat("####.##"); title = df.format(book.getSerieIndex()) + " - " + title; } } else if (Option.contains(options, Option.INCLUDE_TIMESTAMP)) { title = title + " [" + CatalogManager.titleDateFormat.format(book.getTimestamp()) + "]"; } else if (!Option.contains(options, Option.DONOTINCLUDE_RATING) && !currentProfile.getSuppressRatingsInTitles()) { if (book.getRating() != BookRating.NOTRATED) { title = MessageFormat.format(Localization.Main.getText("bookentry.rated"), title, LocalizationHelper.getEnumConstantHumanName(book.getRating())); } } // #c2o-212 // Special handling for the list of books within a tag! if (currentProfile.getSortTagsByAuthor() && getCatalogType().equals(Constants.TAGLIST_TYPE)) { title = (currentProfile.getDisplayAuthorSort() ? book.getAuthorSort() : book.getListOfAuthors()) + " - " + title; } String urn = "calibre:book:" + book.getId(); if (logger.isTraceEnabled()) logger.trace("getBookEntry: checking book in the Catalog manager"); // We only need to actually generate the file if not done previously if (book.isDone()) { if (logger.isDebugEnabled()) logger.debug("getBookEntry: SKIPPING generation of full book entry as already done"); } else { if (logger.isTraceEnabled()) logger.trace("getBookEntry: book full entry (not yet done)"); Element entry = JDOMManager.rootElement("entry", JDOMManager.Namespace.Atom, JDOMManager.Namespace.DcTerms, JDOMManager.Namespace.Atom, JDOMManager.Namespace.Xhtml); entry.addContent (JDOMManager.element("title").addContent(book.getTitle())); entry.addContent(JDOMManager.element("id").addContent("urn:book:" + book.getUuid())); entry.addContent(FeedHelper.getUpdatedTag(book.getLatestFileModifiedDate())); // add the navigation links FeedHelper.decorateElementWithNavigationLinks(entry, pBreadcrumbs, book.getTitle(), fullEntryUrl, true); // add the required data to the book entry decorateBookEntry(entry, book, true); // write the element to the files createFilesFromElement(entry, filename, HtmlManager.FeedType.BookFullEntry, true); if (currentProfile.getGenerateIndex()) { logger.debug("getBookEntry: indexing book"); // index the book // TODO We need to work out what should be stored for image URI's when // TODO we are embedding images as hexencoded strings in the html files. // TODO We probably want pointers to the actual image files (eith stored // TODO in the catalog or the calibre library) instead. IndexManager.indexBook(book, CatalogManager.htmlManager.getHtmlFilename(fullEntryUrl), CatalogManager.thumbnailManager.getThumbnailUrl(book)); } } Element entry = FeedHelper.getBookEntry(title, urn, book.getLatestFileModifiedDate()); // add the required data to the book entry decorateBookEntry(entry, book, false); // add a full entry link to the partial entry if (logger.isTraceEnabled()) logger.trace("getBookEntry: add a full entry link to the partial entry"); entry.addContent(FeedHelper.getFullEntryLink(fullEntryUrl)); book.setReferenced(); book.setDone(); return entry; } }