package com.gmail.dpierron.calibre.opds;
import com.gmail.dpierron.calibre.cache.CachedFile;
import com.gmail.dpierron.calibre.cache.CachedFileManager;
import com.gmail.dpierron.calibre.configuration.ConfigurationManager;
import com.gmail.dpierron.calibre.opds.JDOMManager.Namespace;
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.UnsupportedEncodingException;
import java.io.File;
import java.net.URLEncoder;
import java.util.Calendar;
import java.util.Date;
public class FeedHelper {
private final static Logger logger = LogManager.getLogger(ImageManager.class);
/**
* An Acquisition Feed with newly released OPDS Catalog Entries. These Acquisition Feeds typically contain a subset of the OPDS Catalog
* Entries in an OPDS Catalog based on the publication date of the Publication
*/
private final static String RELATION_SORT_NEW = "http://opds-spec.org/sort/new";
/**
* An Acquisition Feed with popular OPDS Catalog Entries. These Acquisition Feeds typically contain a subset of the OPDS Catalog
* Entries in an OPDS Catalog based on a numerical ranking criteria.
*/
private final static String RELATION_SORT_POPULAR = "http://opds-spec.org/sort/popular";
/**
* An Acquisition Feed with featured OPDS Catalog Entries. These Acquisition Feeds typically contain a subset of the OPDS Catalog
* Entries in an OPDS Catalog that have been selected for promotion by the OPDS Catalog provider. No order is implied.
*/
private final static String RELATION_FEATURED = "http://opds-spec.org/featured";
/**
* An Acquisition Feed with recommended OPDS Catalog Entries. These Acquisition Feeds typically contain a subset of the OPDS Catalog
* Entries in an OPDS Catalog that have been selected specifically for the user.
*/
private final static String RELATION_RECOMMENDED = "http://opds-spec.org/recommended";
/**
* A link to a downloadable book
*/
private final static String RELATION_ACQUISITION = "http://opds-spec.org/acquisition";
/**
* The following page in a paginated Acquisition Feed
*/
public final static String RELATION_NEXT = "next";
/**
* The previous page in a paginated Acquisition Feed
*/
public final static String RELATION_PREV = "prev";
/**
* The first page in a paginated Acquisition Feed
*/
public final static String RELATION_FIRST = "first";
/**
* The last page in a paginated Acquisition Feed
*/
public final static String RELATION_LAST = "last";
/**
* Atom relation for an alternate link - only used for full entry links (see LINKTYPE_FULLENTRY)
*/
private final static String RELATION_ALTERNATE = "alternate";
/**
* A related or suggested Acquisition Feed. An example would be a "related" link from the newest releases in a category to the most
* popular in a category.
*/
private final static String RELATION_RELATED = "related";
/**
* A link to the same page (self-link)
*/
private final static String RELATION_SELF = "self";
/**
* A link to the start page of the catalog
*/
private final static String RELATION_START = "start";
/**
* A breadcrumb link
*/
private final static String RELATION_BREADCRUM = "breadcrumb";
/**
* A link to an author of the item
*/
public final static String RELATION_AUTHOR = "author";
/**
* a graphical Resource associated to the OPDS Catalog Entry
*/
private final static String RELATION_COVER = "http://opds-spec.org/image";
/**
* a reduced-size version of a graphical Resource associated to the OPS Catalog Entry
*/
private final static String RELATION_THUMBNAIL = "http://opds-spec.org/image/thumbnail";
/**
* a link from a partial book entry in a catalog to a full book entry in a separate entry document
*/
private final static String LINKTYPE_FULLENTRY = "application/atom+xml;type=entry;profile=opds-catalog";
/**
* a navigation link, i.e. to another catalog
*/
public final static String LINKTYPE_NAVIGATION = "application/atom+xml;profile=opds-catalog;kind=navigation";
/**
* a link to an html page - external links use this type, in our catalogs
*/
private final static String LINKTYPE_HTML = "text/html";
/**
* a link to a jpeg image
*/
private final static String LINKTYPE_JPEG = "image/jpg";
private final static String LINKTYPE_PNG= "image/png";
/* ---------- ELEMENTS -----------*/
/**
* create the root of an OPDS feed
*
* @param breadcrumbs the navigation elements
* @param pTitle the title of the feed
* @param urn the identifier of the feed
* @param urlExt the URL of the feed (relative to the base URL)
* @return a 'feed' element
*/
public static Element getFeedRootElement(Breadcrumbs breadcrumbs, String pTitle, String urn, String urlExt, boolean inSubDir) {
Element feed = getAtomElement(true,Constants.OPDS_ELEMENT_FEED, pTitle, urn, null, LINKTYPE_NAVIGATION, null, true, null);
// updated tag
Element updated = getUpdatedTag();
feed.addContent(updated);
decorateElementWithNavigationLinks(feed, breadcrumbs, pTitle, urlExt, false);
return feed;
}
/**
* Generate a link to a catalog entry adding an updated element
*
* @param pTitle
* @param urn
* @param filename
* @param pSummary
* @param icon
* @return
*/
public static Element getCatalogEntry(
String pTitle,
String urn,
String filename,
String pSummary,
String icon) {
Element result = getAtomElement(false, Constants.OPDS_ELEMENT_ENTRY, pTitle, urn, filename, pSummary, false, icon);
// add updated
result.addContent(getUpdatedTag());
return result;
}
/**
* Generate a link to a book details entry
*
* @param pTitle
* @param urn
* @param timestamp
* @return
*/
public static Element getBookEntry(String pTitle, String urn, long timestamp) {
Element result = getAtomElement(false, Constants.OPDS_ELEMENT_ENTRY, pTitle, urn, null, null, null, (String) null, false, null);
// add updated
result.addContent(getUpdatedTag(timestamp));
return result;
}
/**
* Generate a link to the 'About entry'
*
* @param title
* @param urn
* @param url
* @param summary
* @param icon
* @return
*/
public static Element getAboutEntry(String title, String urn, String url, String summary, String icon) {
Element result = getAtomElement(false, Constants.OPDS_ELEMENT_ENTRY, title, urn, url, LINKTYPE_HTML, summary, true, icon);
// add updated
result.addContent(getUpdatedTag());
return result;
}
public static Element getExternalLinkEntry(String title, String summary, boolean opdsLink, String urn, String url, String icon) {
Element result = getAtomElement(false, Constants.OPDS_ELEMENT_ENTRY, title, urn, url,
opdsLink ? LINKTYPE_NAVIGATION : LINKTYPE_HTML, summary, false, icon);
// add updated
result.addContent(getUpdatedTag());
return result;
}
/* ---------- LINKS -----------*/
public static Element getNavigationLink(String url, String navType, String title) { return getLinkElement(url, LINKTYPE_NAVIGATION, navType, title); }
public static Element getNextLink(String url, String title) {
return getLinkElement(url, LINKTYPE_NAVIGATION, RELATION_NEXT, title);
}
public static Element getFullEntryLink(String url) {
return getLinkElement(url, LINKTYPE_FULLENTRY, RELATION_ALTERNATE, null);
}
public static Element getRelatedLink(String url, String title) {
return getLinkElement(url, LINKTYPE_NAVIGATION, RELATION_RELATED, title);
}
public static Element getRelatedHtmlLink(String url, String title) {
return getLinkElement(url, LINKTYPE_HTML, RELATION_RELATED, title);
}
public static Element getAcquisitionLink(String url, String mimeType, String title, String size) {
Element link = getLinkElement(url, mimeType, RELATION_ACQUISITION, title);
if (Helper.isNotNullOrEmpty(size)) {
link.setAttribute("displaysize", size);
}
return link;
}
/**
* Add an image link (cover or thumbnail)
*
* @param url
* @param isCover
* @return
*/
public static Element getImageLink(String url, boolean isCover) {
return getLinkElement(url,
url.toUpperCase().endsWith(".PNG") ? LINKTYPE_PNG : LINKTYPE_JPEG,
isCover ? RELATION_COVER : RELATION_THUMBNAIL, null);
}
public static Element getFeaturedLink(String url, String title) {
return getLinkElement(url, LINKTYPE_NAVIGATION, RELATION_FEATURED, title);
}
/**
* Decorate a root element
* (feed or entry, in the case of a full book entry) with the start and self links, and the breadcrumb navigation tree
*
* @param feed the feed to decorate
* @param breadcrumbs the breadcrumbs retracing steps to the root
* @param title the title of the page
* @param catalogFilename the url (filename) of the page being decorated
* @param isEntry if true, the document is a full entry, if false, it's a catalog
*/
public static void decorateElementWithNavigationLinks(Element feed, Breadcrumbs breadcrumbs, String title, String catalogFilename, boolean isEntry) {
if (feed == null)
return;
assert breadcrumbs != null;
assert catalogFilename!= null;
assert catalogFilename.endsWith(Constants.XML_EXTENSION) || catalogFilename.endsWith(Constants.HTML_EXTENSION)
: "Program Error: url should end with .xml extension";
if (catalogFilename.contains("custom")) {
int dummy = 1;
}
// We want to get past any folder separators to get to the base filename;
int pos = 0;
while (catalogFilename.substring(pos).contains(Constants.FOLDER_SEPARATOR))
pos = catalogFilename.indexOf(Constants.FOLDER_SEPARATOR,pos) + 1;
String filename = catalogFilename.substring(pos);
String folder = catalogFilename.substring(0,pos);
pos = folder.indexOf(Constants.CURRENT_PATH_PREFIX); // Also handles parent case!
if (pos != -1)
folder = folder.substring(pos+Constants.CURRENT_PATH_PREFIX.length());
feed.addContent(getLinkElement(Constants.CURRENT_PATH_PREFIX + filename, isEntry ? LINKTYPE_FULLENTRY : LINKTYPE_NAVIGATION, RELATION_SELF, title));
// add a "start" link to the catalog main page
String startUrl = (folder.length() == 0 ? Constants.CURRENT_PATH_PREFIX : Constants.PARENT_PATH_PREFIX)
+ CatalogManager.getInitialUr() + Constants.XML_EXTENSION;
// c2o-87 - Title should use value from settings
feed.addContent(getLinkElement(startUrl,
LINKTYPE_NAVIGATION,
RELATION_START,
ConfigurationManager.getCurrentProfile().getCatalogTitle()));
// add a navigation link to every breadcrumb in the hierarchy
// Special treatment for first breadcrumb (start URL)
if (breadcrumbs.size() > 0) {
// Add breadcrumb links
for (int i = 0 ; i < breadcrumbs.size() ; i++) {
Breadcrumb breadcrumb = breadcrumbs.elementAt(i);
String breadcrumbUrl = breadcrumb.url;
while (breadcrumbUrl.substring(pos).contains(Constants.FOLDER_SEPARATOR))
pos = breadcrumbUrl.indexOf(Constants.FOLDER_SEPARATOR,pos) + 1;
String breadcrumbFilename = breadcrumbUrl.substring(pos);
String breadcrumbFolder = breadcrumbUrl.substring(0,pos);
pos = breadcrumbFolder.indexOf(Constants.CURRENT_PATH_PREFIX); // Also handles parent case!
if (pos != -1)
breadcrumbFolder = breadcrumbFolder.substring(pos+Constants.CURRENT_PATH_PREFIX.length());
feed.addContent(getLinkElement((breadcrumbFolder.equals(folder)
? Constants.CURRENT_PATH_PREFIX
: Constants.PARENT_PATH_PREFIX + breadcrumbFolder)
+ breadcrumbFilename,
LINKTYPE_NAVIGATION,
RELATION_BREADCRUM,
breadcrumb.title));
}
}
}
/* ---------- METADATA ----------*/
public static Element getDublinCoreLanguageElement(String lang) {
Element result = JDOMManager.element("language", Namespace.DcTerms);
result.setText(lang);
return result;
}
public static Element getDublinCorePublisherElement(String publisher) {
Element result = JDOMManager.element("publisher", Namespace.DcTerms);
result.setText(publisher);
return result;
}
public static Element getCategoryElement(String term) {
Element result = JDOMManager.element("category");
result.setAttribute("term", term);
return result;
}
/* ---------- UTILITIES -----------*/
/**
* URL encode a string. Any embedded slashes are NOT encoded
*/
public static String urlEncode(String s) {
return urlEncode(s, false);
}
/**
* URL encode a string with control over how special characters are handled
*
* @param s the string to be encoded
* @param doNotEncodeSlashOrColon if true, slashes and colons will not be encoded
* This ,eams sequences like http:// stay intact
*/
public static String urlEncode(String s, boolean doNotEncodeSlashOrColon) {
try {
String result = s;
if (doNotEncodeSlashOrColon) {
result = result.replace("/", "HERELIESASLASH_ICIUNSLASH");
result = result.replace(":", "HERELIESACOLON_ICIUNSLASH");
}
result = URLEncoder.encode(result, "utf-8");
// this dumb java converts spaces to "+" and I don't like it
result = result.replace("+", "%20");
if (doNotEncodeSlashOrColon) {
result = result.replace("HERELIESASLASH_ICIUNSLASH", "/");
result = result.replace("HERELIESACOLON_ICIUNSLASH", ":");
}
return result;
} catch (UnsupportedEncodingException e) {
// we don't give a damn
return null;
}
}
/* ---------- PRIVATE -----------*/
private static Element getFeedAuthorElement() {
return getFeedAuthorElement(Constants.AUTHORNAME, Constants.HOME_URL, Constants.AUTHOREMAIL);
}
private static Element getFeedAuthorElement(String name, String uri, String email) {
Element author = JDOMManager.element(Constants.OPDS_ELEMENT_AUTHOR);
if (Helper.isNotNullOrEmpty(author))
author.addContent(JDOMManager.element(Constants.OPDS_ELEMENT_NAME).addContent(name));
if (Helper.isNotNullOrEmpty(uri))
author.addContent(JDOMManager.element( Constants.OPDS_ELEMENT_URI).addContent(uri));
if (Helper.isNotNullOrEmpty(email))
author.addContent(JDOMManager.element(Constants.OPDS_ELEMENT_EMAIL).addContent(email));
return author;
}
private static Element getUpdatedTag() {
// TODO: Revisit sinc minimizChangedFiles removed as an option
// TODO: prbably best to act as if it was set?
// if (!ConfigurationManager.getCurrentProfile().getMinimizeChangedFiles()) {
// Calendar c = Calendar.getInstance();
// return getUpdatedTag(c);
// } else {
// DP: return fake updated time - Oh, my birthday, what a coincidence ;)
return JDOMManager.element("updated").addContent("1973-01-26T08:00:00Z");
// }
}
private static String getDateAsIsoDate(Date d) {
Calendar c = Calendar.getInstance();
c.setTime(d);
return getDateAsIsoDate(c);
}
private static String getDateAsIsoDate(Calendar c) {
StringBuffer result = new StringBuffer();
result.append(Helper.leftPad("" + c.get(Calendar.YEAR), '0', 4));
result.append('-');
result.append(Helper.leftPad("" + (c.get(Calendar.MONTH) + 1), '0', 2));
result.append('-');
result.append(Helper.leftPad("" + c.get(Calendar.DAY_OF_MONTH), '0', 2));
result.append('T');
result.append(Helper.leftPad("" + c.get(Calendar.HOUR), '0', 2));
result.append(':');
result.append(Helper.leftPad("" + c.get(Calendar.MINUTE), '0', 2));
result.append(':');
result.append(Helper.leftPad("" + c.get(Calendar.SECOND), '0', 2));
result.append('Z');
return result.toString();
}
public static Element getUpdatedTag(long timeInMilli) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(timeInMilli);
return getUpdatedTag(c);
}
private static Element getUpdatedTag(Calendar c) {
return JDOMManager.element("updated").addContent(getDateAsIsoDate(c));
}
public static Element getPublishedTag(Date d) {
return JDOMManager.element("published").addContent(getDateAsIsoDate(d));
}
/**
*
* @param url
* @param urlType
* @param urlRelation
* @param title
* @return
*/
public static Element getLinkElement(String url, String urlType, String urlRelation, String title) {
Element link = JDOMManager.element(Constants.OPDS_ELEMENT_LINK);
if (urlType != null && urlRelation != null
&& urlType.equals(LINKTYPE_NAVIGATION)
&& (urlRelation.equals(RELATION_NEXT) || urlRelation.equals(RELATION_PREV) || urlRelation.equals(RELATION_FIRST) || urlRelation.equals(RELATION_LAST))) {
// Next URL's mean we are already in a folder, so ensure we go up a level as part of the URL (c2o-104)
if (! url.startsWith("../"))
url = "../" + url;
}
link.setAttribute("href", url);
if (Helper.isNotNullOrEmpty(urlType)) {
link.setAttribute("type", urlType);
// #c20-277 Set download attribute for acquisition links
// #c2o-280 undo previ/us chande as download attiibute now added via XRL
// if (Helper.isNotNullOrEmpty(urlRelation) && urlRelation.equals(RELATION_ACQUISITION)) {
// String filename = url.substring(url.lastIndexOf('/') + 1);
// link.setAttribute("download", filename);
// }
}
if (Helper.isNotNullOrEmpty(urlRelation))
link.setAttribute("rel", urlRelation);
if (Helper.isNotNullOrEmpty(title))
link.setAttribute("title", title);
return link;
}
private static Element getAtomElement(boolean isRoot,
String pElement,
String pTitle,
String urn,
String filename,
String pSummary,
boolean includeAuthor,
String icon) {
return getAtomElement(isRoot, pElement, pTitle, urn, filename, LINKTYPE_NAVIGATION, pSummary, includeAuthor, icon);
}
private static Element getAtomElement(boolean isRoot,
String pElement,
String pTitle,
String urn,
String url,
String urlType,
String pSummary,
boolean includeAuthor,
String icon) {
return getAtomElement(isRoot, pElement, pTitle, urn, url, urlType, null, // Relation not required
pSummary, includeAuthor, icon);
}
private static Element getAtomElement(boolean isRoot,
String elementName,
String title,
String id,
String url,
String urlType,
String urlRelation,
String content,
boolean includeAuthor,
String icon) {
Element contentElement = null;
if (Helper.isNotNullOrEmpty(content)) {
contentElement = JDOMManager.element("content").addContent(content);
contentElement.setAttribute("type", "text");
}
Element element;
if (isRoot)
element = JDOMManager.rootElement(elementName, Namespace.Atom, Namespace.DcTerms, Namespace.Atom, Namespace.Xhtml, Namespace.Opds);
else
element = JDOMManager.element(elementName);
// title
Element titleElement = JDOMManager.element("title").addContent(title);
element.addContent(titleElement);
// id
Element idElement = JDOMManager.element("id").addContent(id);
element.addContent(idElement);
// content
if (contentElement != null) {
element.addContent(contentElement);
}
// link
if (Helper.isNotNullOrEmpty(url)) {
element.addContent(getLinkElement(url, urlType, urlRelation, null));
}
// icon link
if (Helper.isNotNullOrEmpty(icon)) {
Element iconElt = getLinkElement(icon, "image/png", "http://opds-spec.org/image/thumbnail", null);
element.addContent(iconElt);
}
// add the feed author
if (includeAuthor)
element.addContent(getFeedAuthorElement());
return element;
}
/**
* We changed the standard for naming files.
* This carries out the check if a file exists with the old naming
* standard and and if necessary renames it to the new standard.
* It use the CachedFile class to try and minimise any I/O from repeated checks
*
* @param newfile
* @param oldfile
*/
public static void checkFileNameIsNewStandard (CachedFile newfile, File oldfile) {
if (! newfile.exists() && oldfile.exists()) {
oldfile.renameTo(newfile);
newfile.clearCachedInformation(); // Clear cached information
CachedFileManager.removeCachedFile(oldfile);
logger.info("File " + oldfile.getName() + " renamed to " + newfile.getName());
}
}
}