package com.gmail.dpierron.calibre.opds; import com.gmail.dpierron.calibre.configuration.ConfigurationManager; import com.gmail.dpierron.calibre.datamodel.DataModel; import com.gmail.dpierron.tools.i18n.Localization; import com.gmail.dpierron.tools.Helper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jdom2.*; import org.jdom2.input.JDOMParseException; import org.jdom2.input.SAXBuilder; import org.jdom2.input.sax.SAXHandlerFactory; import org.jdom2.input.sax.XMLReaders; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; import org.w3c.tidy.Tidy; import javax.xml.transform.Result; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamSource; import java.io.*; import java.text.DateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; public class JDOMManager { private final static Logger logger = LogManager.getLogger(JDOMManager.class); private static JDOMFactory factory; private static XMLOutputter outputter; private static XMLOutputter serializer; private static TransformerFactory transformerFactory; private static Transformer bookFullEntryTransformer; private static Transformer catalogTransformer; private static Transformer mainTransformer; private static SAXBuilder sb; public static void reset() { factory = null; outputter = null; serializer = null; transformerFactory = null; bookFullEntryTransformer = null; catalogTransformer = null; mainTransformer = null; sb = null; } /** * Set the paremeters for creating the header.html file * * @return */ public static Transformer getIncludeTransformer(String xslname) { Transformer headerTransformer; try { headerTransformer = getTransformerFactory().newTransformer(new StreamSource(ConfigurationManager.getResourceAsStream(xslname))); setParametersOnCatalog(headerTransformer); setIntroParameters(headerTransformer); } catch (TransformerConfigurationException e) { logger.error("getIncludeTransformer(): Error while configuring header transformer", e); headerTransformer = null; } return headerTransformer; } public enum Namespace { Atom("", "http://www.w3.org/2005/Atom"), Opds("opds", "http://opds-spec.org/2010/catalog"), Opf("opf", "http://www.idpf.org/2007/opf"), Dc("dc", "http://purl.org/dc/elements/1.1/"), DcTerms("dcterms", "http://purl.org/dc/terms"), Calibre("calibre", "http://calibre.kovidgoyal.net/2009/metadata"), Xhtml("xhtml", "http://www.w3.org/1999/xhtml"); private org.jdom2.Namespace jdomNamespace; private Namespace(String prefix, String uri) { jdomNamespace = org.jdom2.Namespace.getNamespace(prefix, uri); } public org.jdom2.Namespace getJdomNamespace() { return this.jdomNamespace; } } /** * Set parameters that are common to many the transformers we use. * @param catalogTransformer */ private static void setParametersOnCatalog(Transformer catalogTransformer) { double dh = ConfigurationManager.getCurrentProfile().getCoverHeight(); double dw = 2f / 3f * dh; long lh = (long) Math.floor(dh); long lw = (long) Math.floor(dw); catalogTransformer.setParameter("coverWidth", lw); catalogTransformer.setParameter("coverHeight", lh); dh = ConfigurationManager.getCurrentProfile().getThumbnailHeight(); dw = 2f / 3f * dh; lh = (long) Math.floor(dh); lw = (long) Math.floor(dw); catalogTransformer.setParameter("thumbWidth", lw); catalogTransformer.setParameter("thumbHeight", lh); catalogTransformer.setParameter("generateDownloads", Boolean.toString(ConfigurationManager.getCurrentProfile().getGenerateHtmlDownloads()).toLowerCase()); catalogTransformer.setParameter("libraryTitle", ConfigurationManager.getCurrentProfile().getCatalogTitle()); catalogTransformer.setParameter("i18n.and", Localization.Main.getText("i18n.and")); catalogTransformer.setParameter("i18n.backToMain", Localization.Main.getText("i18n.backToMain")); catalogTransformer.setParameter("i18n.downloads", Localization.Main.getText("i18n.downloads")); catalogTransformer.setParameter("i18n.links", Localization.Main.getText("i18n.links")); catalogTransformer.setParameter("i18n.downloadfile", Localization.Main.getText("i18n.downloadfile")); catalogTransformer.setParameter("i18n.coversection", Localization.Main.getText("i18n.coversection")); catalogTransformer.setParameter("i18n.summarysection", Localization.Main.getText("i18n.summarysection")); catalogTransformer.setParameter("i18n.downloadsection", Localization.Main.getText("i18n.downloadsection")); catalogTransformer.setParameter("i18n.relatedsection", Localization.Main.getText("i18n.relatedsection")); catalogTransformer.setParameter("i18n.linksection", Localization.Main.getText("i18n.linksection")); catalogTransformer.setParameter("browseByCover", Boolean.toString(ConfigurationManager.getCurrentProfile().getBrowseByCover()).toLowerCase()); catalogTransformer.setParameter("generateIndex", Boolean.toString(ConfigurationManager.getCurrentProfile().getGenerateIndex()).toLowerCase()); // We only want to add the Date Generated to the bottom of each catalog page if // we have not elected to try and minimise the number of files changed each run // (it will still be added to the top page) // TODO: decide if we never want this on each page? // TODO: Now that minimizechangedfiles removed need to rethink // TODO: Probably best to acti as if minimizechangedfiles had been set // if (ConfigurationManager.getCurrentProfile().getMinimizeChangedFiles()) { catalogTransformer.setParameter("i18n.dateGenerated",""); // } else { // String dateGenerated = // DateFormat.getDateInstance(DateFormat.DEFAULT, ConfigurationManager.getCurrentProfile().getLanguage()).format(new Date()); // catalogTransformer.setParameter("i18n.dateGenerated", Localization.Main.getText("i18n.dateGenerated", dateGenerated)); // } // Add book count if not generating ALl Books (which gives count if present) // headerTransformer.setParameter("programName", Constants.PROGNAME); // headerTransformer.setParameter("programVersion", Constants.PROGVERSION + Constants.BZR_VERSION); // headerTransformer.setParameter("bookCount", Localization.Main.getText("bookword.many", DataModel.getListOfBooks().size())); String dateGenerated = DateFormat.getDateInstance(DateFormat.DEFAULT, ConfigurationManager.getCurrentProfile().getLanguage()).format(new Date()); String sizeText = "("+ Localization.Main.getText("bookword.many", DataModel.getListOfBooks().size()) +")"; catalogTransformer.setParameter("i18n.dateGenerated", Localization.Main.getText("i18n.dateGenerated",dateGenerated) + " " + sizeText); } /** * set the transformer that is used for the top level page. * * @return */ public static Transformer getMainCatalogTransformer() { if (mainTransformer == null) { try { mainTransformer = getTransformerFactory().newTransformer(new StreamSource(ConfigurationManager.getResourceAsStream(Constants.CATALOG_XSL))); setParametersOnCatalog(mainTransformer); setIntroParameters(mainTransformer); } catch (TransformerConfigurationException e) { logger.error("getMainCatalogTransformer(): Error while configuring catalog transformer", e); mainTransformer = null; } } return mainTransformer; } public static Transformer getCatalogTransformer() { if (catalogTransformer == null) { try { catalogTransformer = getTransformerFactory().newTransformer(new StreamSource(ConfigurationManager.getResourceAsStream(Constants.CATALOG_XSL))); setParametersOnCatalog(catalogTransformer); catalogTransformer.setParameter("programName", ""); // Set to empty for all pages except top level } catch (TransformerConfigurationException e) { logger.error("getCatalogTransformer(): Error while configuring catalog transformer", e); catalogTransformer = null; } } return catalogTransformer; } /** * Transformer used on a Book fulle entry. * * @return */ public static Transformer getBookFullEntryTransformer() { if (bookFullEntryTransformer == null) { try { bookFullEntryTransformer = getTransformerFactory().newTransformer(new StreamSource(ConfigurationManager.getResourceAsStream(Constants.FULLENTRY_XSL))); setParametersOnCatalog(bookFullEntryTransformer); } catch (TransformerConfigurationException e) { logger.error("getCatalogTransformer(): Error while configuring book full entry transformer", e); bookFullEntryTransformer = null; } } return bookFullEntryTransformer; } /** * Set parameters that reate to the 'about' information for calibre2opds * * @param transformer * @return */ private static Transformer setIntroParameters (Transformer transformer) { if (transformer != null) { boolean includeAbout = ConfigurationManager.getCurrentProfile().getIncludeAboutLink(); transformer.setParameter("programName", Constants.PROGNAME); transformer.setParameter("programVersion", Constants.PROGVERSION + Constants.BZR_VERSION); // Set up options to display t bottom of top pge in catalog // Always include these options on top page transformer.setParameter("intro.goal", Localization.Main.getText("intro.goal")); transformer.setParameter("intro.wiki.title", Localization.Main.getText("intro.wiki.title")); transformer.setParameter("intro.wiki.url", Localization.Main.getText("intro.wiki.url")); transformer.setParameter("intro.userguide", Localization.Main.getText("gui.menu.help.userGuide")); transformer.setParameter("intro.userguide.url", Constants.USERGUIDE_URL); transformer.setParameter("intro.developerguide", Localization.Main.getText("gui.menu.help.developerGuide")); transformer.setParameter("intro.developerguide.url", Constants.DEVELOPERGUIDE_URL ); // Only include these options on top page if the about section not suppresssed transformer.setParameter("intro.team.title", includeAbout ? Localization.Main.getText("intro.team.title") : ""); transformer.setParameter("intro.team.list1", includeAbout ? Localization.Main.getText("intro.team.list1") : ""); transformer.setParameter("intro.team.title2", includeAbout ? Localization.Main.getText("intro.team.title2") : ""); transformer.setParameter("intro.team.list2", includeAbout ? Localization.Main.getText("intro.team.list2") : ""); transformer.setParameter("intro.team.list3", includeAbout ? Localization.Main.getText("intro.team.list3") : ""); transformer.setParameter("intro.team.list4", includeAbout ? Localization.Main.getText("intro.team.list4") : ""); transformer.setParameter("intro.thanks.1", includeAbout ? Localization.Main.getText("intro.thanks.1") : ""); transformer.setParameter("intro.thanks.2", includeAbout ? Localization.Main.getText("intro.thanks.2") : ""); } return transformer; } /** * Code common to setting up all transformer factories * * @return */ public static TransformerFactory getTransformerFactory() { if (transformerFactory == null) { transformerFactory = TransformerFactory.newInstance(); } return transformerFactory; } public static void setTransformerFactory(TransformerFactory transformerFactory) { transformerFactory = transformerFactory; } public static JDOMFactory getFactory() { if (factory == null) { factory = new DefaultJDOMFactory(); } return factory; } public static SAXBuilder getSaxBuilder() { if (sb == null) { sb = new SAXBuilder(); } return sb; } public static XMLOutputter getPrettyXML() { if (outputter == null) { outputter = new XMLOutputter(Format.getPrettyFormat()); } return outputter; } public static XMLOutputter getRawXml() { if (serializer == null) { serializer = new XMLOutputter(Format.getRawFormat()); } return serializer; } public static XMLOutputter getCompactXML() { if (serializer == null) { serializer = new XMLOutputter(Format.getCompactFormat()); } return serializer; } public static Element rootElement(String name, Namespace namespace, Namespace... declaredNamespaces) { Element result = element(name, namespace); for (Namespace declaredNamespace : declaredNamespaces) { result.addNamespaceDeclaration(declaredNamespace.getJdomNamespace()); } return result; } public static Element element(String name, Namespace namespace) { Element result = getFactory().element(name); if (namespace != null) { result.setNamespace(namespace.getJdomNamespace()); } return result; } public static Element element(String name) { return element(name, Namespace.Atom); } public static Element newParagraph() { return element("p", Namespace.Atom); } private static Tidy tidyForTidyInputStream = null; /** * Routine to tidy up the HTML in the Summary field * * @param in * @return * @throws JDOMException * @throws IOException */ public static List<Element> tidyInputStream(InputStream in) throws JDOMException, IOException { List<Element> result = null; // initializing tidy if (logger.isTraceEnabled()) logger.trace("tidyInputStream: initializing tidy"); if (tidyForTidyInputStream == null) { tidyForTidyInputStream = new Tidy(); tidyForTidyInputStream.setShowWarnings(false); tidyForTidyInputStream.setXmlOut(true); tidyForTidyInputStream.setInputEncoding("utf-8"); tidyForTidyInputStream.setQuiet(true); tidyForTidyInputStream.setDropEmptyParas(false); } ByteArrayOutputStream out = new ByteArrayOutputStream(); if (logger.isTraceEnabled()) logger.trace("tidyInputStream: parsing with Tidy"); try { tidyForTidyInputStream.parseDOM(in, out); } finally { out.close(); } String text2 = new String(out.toByteArray()); SAXBuilder sb = new SAXBuilder(XMLReaders.NONVALIDATING, (SAXHandlerFactory)null, (JDOMFactory)null); if (logger.isTraceEnabled()) logger.trace("tidyInputStream: building doc"); Document doc = sb.build(new StringReader(text2)); Element html = doc.getRootElement(); if (! html.getName().equalsIgnoreCase("html")) { if (logger.isTraceEnabled()) logger.trace("tidyInputStream: no html tag found"); } else { if (logger.isTraceEnabled()) logger.trace("tidyInputStream: found html tag"); for (Object o : html.getChildren()) { if (o instanceof Element) { Element child = (Element) o; if (! child.getName().equalsIgnoreCase("body")) { if (logger.isTraceEnabled()) logger.trace("tidyInputStream: no body tag found"); } else { if (logger.isTraceEnabled()) logger.trace("tidyInputStream: found body tag"); (result = new ArrayList<Element>()).addAll(child.getChildren()); } } } } if (logger.isTraceEnabled()) logger.trace("tidyInputStream: completed tidy"); return result; } /** * * @param text text to convert to HTML * @return */ public static List<Element> convertHtmlTextToXhtml(String text) { List<Element> result = null; // if (logger.isTraceEnabled()) // logger.trace("Comment to convert: " + Database.stringToHex(text)); if (Helper.isNotNullOrEmpty(text)) { if (!text.startsWith("<")) { // plain text if (logger.isTraceEnabled())logger.trace("convertHtmlTextToXhtml: plain text"); StringBuffer sb = new StringBuffer(); if (Helper.isNotNullOrEmpty(text)) { List<String> strings = Helper.tokenize(text, "\n", true); for (String string : strings) { if (Helper.isNullOrEmpty(string)) { sb.append("<p />"); } else { sb.append("<p>"); sb.append(string); sb.append("</p>\n"); } } } text = sb.toString(); } // tidy the text if (logger.isTraceEnabled()) logger.trace("convertHtmlTextToXhtml: tidy the text"); try { String noNL = text.replaceAll("\\n",""); result = tidyInputStream(new ByteArrayInputStream(noNL.getBytes("utf-8"))); // result = tidyInputStream(new ByteArrayInputStream(text.getBytes("utf-8"))); String afterTIDY = result.toString(); int dummy = 1; } catch (JDOMParseException j) { if (logger.isDebugEnabled()) logger.trace("convertHtmlTextToXhtml: caught JDOMParseException in the tidy process"); if (logger.isTraceEnabled()) logger.trace( "" + j); tidyForTidyInputStream = null; // Force a new clean object to be gebnerated for next time around } catch (Exception ee) { if (logger.isDebugEnabled()) logger.debug("convertHtmlTextToXhtml: caught exception in the tidy process", ee); tidyForTidyInputStream = null; // Force a new clean object to be gebnerated for next time around } catch (Throwable t) { logger.error("convertHtmlTextToXhtml: caught throwable in the tidy process", t); tidyForTidyInputStream = null; // Force a new clean object to be gebnerated for next time around } if (result != null) { if (logger.isTraceEnabled()) logger.trace("convertHtmlTextToXhtml: returning XHTML"); } else { if (Helper.isNotNullOrEmpty(text)) logger.debug("convertHtmlTextToXhtml: Cannot convert comment text\n" + text); } } return result; } }