package com.gmail.dpierron.calibre.opds;
/**
* Class for implementing the Series type 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.*;
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 SeriesSubCatalog extends BooksSubCatalog {
private final static Logger logger = LogManager.getLogger(SeriesSubCatalog.class);
private final static Collator collator = Collator.getInstance(ConfigurationManager.getLocale());
private List<Series> series;
private Map<Series, List<Book>> mapOfBooksBySerie;
public SeriesSubCatalog(List<Object> stuffToFilterOut, List<Book> books) {
super(stuffToFilterOut, books);
setCatalogType(Constants.SERIESLIST_TYPE);
}
/**
* Construct a series object from a list of books
*
* @param books
*/
public SeriesSubCatalog(List<Book> books) {
super(books);
setCatalogType(Constants.SERIESLIST_TYPE);
}
/*
public List<Series> getObjectList() {
return getSeries();
}
*/
/**
* Get the series list for the books associated with this sub-catalog
* If it is not populated, then do so in alphabetical order
* (taking into account leading noise words).
*/
public List<Series> getSeries() {
if (series == null) {
series = new LinkedList<Series>();
for (Book book : getBooks()) {
if (book.getSeries() != null && !series.contains(book.getSeries()))
series.add(book.getSeries());
}
final Language bookLang = getBooks().get(0).getFirstBookLanguage();
// sort the series alphabetically
Collections.sort(series, new Comparator<Series>() {
public int compare(Series o1, Series o2) {
return Helper.checkedCollatorCompareIgnoreCase((o1 == null ? "" : o1.getTitleToSplitByLetter()),
(o2 == null ? "" : o2.getTitleToSplitByLetter()),
collator);
}
});
}
return series;
}
/**
* @return
*/
private Map<Series, List<Book>> getMapOfBooksBySerie() {
if (mapOfBooksBySerie == null) {
mapOfBooksBySerie = new HashMap<Series, List<Book>>();
for (Book book : getBooks()) {
List<Book> books = mapOfBooksBySerie.get(book.getSeries());
if (books == null) {
books = new LinkedList<Book>();
Series serie = book.getSeries();
if (serie != null)
mapOfBooksBySerie.put(serie, books);
}
books.add(book);
}
}
return mapOfBooksBySerie;
}
/**
* Get the list of series for embedding at the current level.
*
* This is used when you want to add a list of series to an existing
* page without creating a link to a new page.
*
* This reoutine is called recursively to generate each file (page or split level)
*
* @param pBreadcrumbs
* @param listSeries // Series to process. Set to null if not known to take from catalog properties
* @param from // Start point - set to 0 if not known
* @param title
* @param summary
* @param urn
* @param pFilename
* @param splitOption
* @param addTheSeriesWordToTheTitle
* @return
* @throws IOException
*/
public List<Element> getListOfSeries(
Breadcrumbs pBreadcrumbs,
List<Series> listSeries, // The list of series to be processed.
boolean inSubDir,
int from, // Start point - set to 0 if not known
String title,
String summary,
String urn,
String pFilename,
SplitOption splitOption,
Boolean addTheSeriesWordToTheTitle) throws IOException {
// Set if not specified from catalog properties
if (listSeries == null) listSeries = getSeries();
if (pFilename == null) pFilename = getCatalogBaseFolderFileName();
Map<String, List<Series>> mapOfSeriesByLetter = null;
List<Element> result;
if (logger.isTraceEnabled()) logger.trace("getListOfSeries: title=" + title);
boolean willSplitByLetter;
if (null == splitOption)
splitOption = SplitOption.SplitByLetter;
switch (splitOption) {
// case DontSplit:
case Paginate:
case DontSplit:
if (logger.isTraceEnabled())
logger.trace("splitOption=" + splitOption);
willSplitByLetter = false;
break;
default:
if (logger.isTraceEnabled())
logger.trace("getListOfSeries: splitOption=" + splitOption + ", series.size()=" + listSeries.size() + ", MaxBeforeSplit==" +
maxBeforeSplit);
willSplitByLetter = (maxSplitLevels != 0) && (listSeries.size() > maxBeforeSplit);
break;
}
if (logger.isTraceEnabled())
logger.trace("getListOfSeries: willSplitByLetter=" + willSplitByLetter);
if (willSplitByLetter) {
mapOfSeriesByLetter = DataModel.splitSeriesByLetter(listSeries);
}
if (from > 0) inSubDir = true;
int pageNumber = Summarizer.getPageNumber(from + 1);
String filename = pFilename + Constants.PAGE_DELIM + Integer.toString(pageNumber);
// list the entries (or split them)
if (willSplitByLetter) {
// split the series list by letter
Breadcrumbs breadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, inSubDir));
result = getListOfSeriesSplitByLetter(breadcrumbs,
mapOfSeriesByLetter,
inSubDir,
title,
urn,
pFilename,
addTheSeriesWordToTheTitle);
} else {
// list the series list
result = new LinkedList<Element>();
for (int i = from; i < listSeries.size(); i++) {
if ((splitOption != SplitOption.DontSplitNorPaginate) && ((i - from) >= maxBeforePaginate)) {
// TODO #c2o-208 Add Previous, First and Last links if needed
Element nextLink =
getSubCatalog(pBreadcrumbs,
listSeries,
true /*inSubDir*/, // Must be in subDir if paginating!
i,
title,
summary,
urn,
pFilename,
splitOption != SplitOption.DontSplitNorPaginate ? SplitOption.Paginate : splitOption, addTheSeriesWordToTheTitle);
result.add(0, nextLink);
int maxPages = Summarizer.getPageNumber(listSeries.size());
break;
} else {
Series serie = listSeries.get(i);
Breadcrumbs breadcrumbs;
// #c2o-204: If we are getting the series for an author then the breadcrumbs are already correct.
if (this.getCatalogFolder().startsWith(Constants.AUTHOR_TYPE) || this.getCatalogFolder().startsWith(Constants.AUTHORLIST_TYPE)) {
breadcrumbs = pBreadcrumbs;
} else {
breadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, inSubDir));
}
Element entry = getDetailedEntry(breadcrumbs, serie, urn, addTheSeriesWordToTheTitle);
if (entry != null) {
result.add(entry);
TrookSpecificSearchDatabaseManager.addSeries(serie, entry);
}
}
}
}
return result;
}
/**
* Get a new Series sub-catalog.
*
* @param pBreadcrumbs
* @param listSeries series, or nuil to derive series from books
* @param inSubDir
* @param from
* @param title
* @param summary
* @param urn
* @param pFilename if null then derived from getCatalogBaseFolderFileName()
* @param splitOption
* @param addTheSeriesWordToTheTitle
* @return
* @throws IOException
*/
public Element getSubCatalog(Breadcrumbs pBreadcrumbs,
List<Series> listSeries,
boolean inSubDir,
int from,
String title,
String summary,
String urn,
String pFilename,
SplitOption splitOption,
Boolean addTheSeriesWordToTheTitle) throws IOException {
if (listSeries == null) listSeries = getSeries();
if (listSeries.size() == 0) {
if (logger.isDebugEnabled()) logger.debug("getSubCatalog: Return 'null' as no series entries found");
return null;
}
if (pFilename == null) pFilename = getCatalogBaseFolderFileName();
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*/);
Breadcrumbs breadcrumbs = Breadcrumbs.addBreadcrumb(pBreadcrumbs, title, urlExt);
// Check for special case where the series name is equal to the split level
String seriesNameUpper = currentProfile.getSortSeriesUsingLibrarySort()
? listSeries.get(0).getName().toUpperCase()
: listSeries.get(0).getSort().toUpperCase();
if (splitOption == SplitOption.SplitByLetter) {
while (listSeries.size() > 0
&& pFilename.toUpperCase().endsWith(Constants.TYPE_SEPARATOR + seriesNameUpper)) {
Series series = listSeries.get(0);
listSeries.remove(0);
Element element;
element = getDetailedEntry(breadcrumbs, series, urn, addTheSeriesWordToTheTitle);
assert element != null;
if (element != null) {
feed.addContent(element);
}
}
}
int catalogSize = listSeries.size();
if (summary == null) summary = catalogSize > 1 ? Localization.Main.getText("series.alphabetical", catalogSize)
: (catalogSize == 1 ? Localization.Main.getText("series.alphabetical.single") : "");
if (urn == null) urn = Constants.INITIAL_URN_PREFIX + Constants.URN_SEPARATOR + Constants.SERIESLIST_TYPE + getCatalogLevel();
boolean willSplitByLetter = checkSplitByLetter(splitOption, catalogSize);
if (willSplitByLetter) {
catalogSize = 0;
} else
catalogSize = listSeries.size();
int maxPages = Summarizer.getPageNumber(catalogSize);
// list the entries (or split them)
List<Element> result = getListOfSeries(pBreadcrumbs,
listSeries,
inSubDir,
from,
title,
summary,
urn,
pFilename,
splitOption,
addTheSeriesWordToTheTitle);
// add the entries to the feed
feed.addContent(result);
Element entry;
String urlInItsSubfolder = optimizeCatalogURL(CatalogManager.getCatalogFileUrl(filename + Constants.XML_EXTENSION, inSubDir));
entry = createPaginateLinks(feed, urlInItsSubfolder, 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_SERIES : Icons.ICON_SERIES);
}
return entry;
}
/**
* @param pBreadcrumbs
* @param mapOfSeriesByLetter
* @param baseTitle
* @param baseUrn
* @param baseFilename
* @param addTheSeriesWordToTheTitle
* @return
* @throws IOException
*/
private List<Element> getListOfSeriesSplitByLetter(
Breadcrumbs pBreadcrumbs,
Map<String, List<Series>> mapOfSeriesByLetter,
boolean inSubDir,
String baseTitle,
String baseUrn,
String baseFilename,
boolean addTheSeriesWordToTheTitle) throws IOException {
if (Helper.isNullOrEmpty(mapOfSeriesByLetter))
return null;
String sTitle = baseTitle;
if (Helper.isNotNullOrEmpty(sTitle))
sTitle = sTitle + ", ";
List<Element> result = new LinkedList<Element>();
SortedSet<String> letters = new TreeSet<String>(mapOfSeriesByLetter.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<Series> seriesInThisLetter = mapOfSeriesByLetter.get(letter);
String letterTitle;
int itemsCount = seriesInThisLetter.size();
if (letter.equals("_"))
letterTitle = Localization.Main.getText("splitByLetter.series.other");
else
letterTitle = Localization.Main.getText("splitByLetter.letter", Localization.Main.getText("seriesword.title"),
letter.length() > 1 ? letter.substring(0,1) + letter.substring(1).toLowerCase() : letter);
Element element = null;
if (itemsCount > 0) {
int maxBeforeSplit=0;
// try and list the items to make the summary
String summary = Summarizer.summarizeSeries(seriesInThisLetter);
element = getSubCatalog(pBreadcrumbs,
seriesInThisLetter,
true, // inSubDir must be true if splitting by letter
0,
letterTitle,
summary,
letterUrn,
letterFilename,
checkSplitByLetter(letter),
addTheSeriesWordToTheTitle);
}
if (element != null)
result.add(element);
}
return result;
}
/**
* Get the base filename that is used to store a given series
*
* Since we always hold a complete set of series at the top level
* the name can be derived purely knowing the series involved.
*
* NOTE. This is NOT used in the case where we are doing the
* sub-set of a series for a given author.
*
* @param serie
* @return
*/
public static String getSeriesFolderFilenameNoLevel(Series serie) {
return getCatalogBaseFolderFileNameIdNoLevelSplit(Constants.SERIES_TYPE, serie.getId(), 1000);
}
/**
* Get the base filename that is used to store a given series
* taking into account any level information
*
* @param serie
* @return
*/
public String getSeriesFolderFilenameWithLevel(Series serie) {
return getCatalogBaseFolderFileNameIdSplit(Constants.SERIES_TYPE, serie.getId(), 1000);
}
/**
* List the books that belong to the given series
*
* @param pBreadcrumbs
* @param seriesObject
* @param opts baseurn
* addTheSeriesWordToTheTitle
* @return
* @throws IOException
*/
// public Element getSeriesEntry(Breadcrumbs pBreadcrumbs, Series serie, String baseurn, boolean addTheSeriesWordToTheTitle) throws IOException {
public Element getDetailedEntry(Breadcrumbs pBreadcrumbs,
Object seriesObject,
Object... opts) throws IOException {
assert seriesObject.getClass().equals(Series.class);
Series serie = (Series)seriesObject;
assert opts[0] != null && opts[0].getClass().equals(String.class);
String baseurn = (String)opts[0];
assert opts[1] != null && opts[1].getClass().equals(Boolean.class);
boolean addTheSeriesWordToTheTitle = (Boolean)opts[1];
if (logger.isDebugEnabled()) logger.debug(pBreadcrumbs + "/" + serie);
CatalogManager.callback.showMessage(pBreadcrumbs.toString());
// We want to avoid incrementing the progress bar if we are not doing
// the top level series sub-catalog
if ( !(isInDeepLevel() || (getCatalogFolder().startsWith(Constants.AUTHOR_TYPE))))
CatalogManager.callback.incStepProgressIndicatorPosition();
List<Book> books = getMapOfBooksBySerie().get(serie);
if (Helper.isNullOrEmpty(books))
return null;
// sort the books by series index
// If the series index is the same, then sort by title within the index.
Collections.sort(books, new Comparator<Book>() {
public int compare(Book o1, Book o2) {
Float index1 = (o1 == null ? Float.MIN_VALUE : o1.getSerieIndex());
Float index2 = (o2 == null ? Float.MIN_VALUE : o2.getSerieIndex());
if (index1 != index2) {
return index1.compareTo(index2);
}
String title1 = o1.getTitleToSplitByLetter();
String title2 = o2.getTitleToSplitByLetter();
return Helper.checkedCollatorCompareIgnoreCase(title1, title2, collator);
}
});
String title = currentProfile.getDisplaySeriesSort() ? serie.getSort() : serie.getName();
if (addTheSeriesWordToTheTitle) {
title = Localization.Main.getText("content.series") + ": " + title;
}
String urn = baseurn + Constants.SERIES_TYPE + Constants.URN_SEPARATOR + serie.getId();
// We need to determine if we are generating a serie within an author?
// If we are we want the file to be in the author folder
// if we are not then we want it at the top level
String filename;
if (getCatalogFolder().startsWith(getCatalogType())) {
filename = getSeriesFolderFilenameWithLevel(serie);
} else {
filename = getCatalogBaseFolderFileNameId(serie.getId());
}
// try and list the items to make the summary
String summary = Summarizer.summarizeBooks(books);
Element result = getListOfBooks(pBreadcrumbs,
books,
true,
0, // Starting at 0
title,
summary,
urn,
filename,
SplitOption.Paginate, // Do not split on letter in Series - it does not really make sense
useExternalIcons // #751211: Use external icons option
? getIconPrefix(true) + Icons.ICONFILE_SERIES : Icons.ICON_SERIES,
null,
Option.INCLUDE_SERIE_NUMBER);
serie.setDone();
return result;
}
}