| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.freemarker.docgen; |
| |
| import static org.freemarker.docgen.DocBook5Constants.*; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.StringReader; |
| import java.io.Writer; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.text.Collator; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.SortedMap; |
| import java.util.TimeZone; |
| import java.util.TreeMap; |
| import java.util.regex.Pattern; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.SAXException; |
| |
| import freemarker.cache.ClassTemplateLoader; |
| import freemarker.cache.FileTemplateLoader; |
| import freemarker.cache.MultiTemplateLoader; |
| import freemarker.cache.TemplateLoader; |
| import freemarker.ext.dom.NodeModel; |
| import freemarker.log.Logger; |
| import freemarker.template.Configuration; |
| import freemarker.template.SimpleHash; |
| import freemarker.template.SimpleScalar; |
| import freemarker.template.Template; |
| import freemarker.template.TemplateException; |
| import freemarker.template.TemplateMethodModelEx; |
| import freemarker.template.TemplateModel; |
| import freemarker.template.TemplateModelException; |
| import freemarker.template.TemplateScalarModel; |
| import freemarker.template.utility.ClassUtil; |
| import freemarker.template.utility.DateUtil; |
| import freemarker.template.utility.DateUtil.DateParseException; |
| import freemarker.template.utility.StringUtil; |
| |
| /** |
| * Generates complete HTML-format documentation from a DocBook 5 (XML) book. |
| * |
| * <p>Usage: First set the JavaBean properties, then call {@link #execute()}; |
| * These must be set: |
| * <ul> |
| * <li>{@link #setSourceDirectory(File)} |
| * <li>{@link #setDestinationDirectory(File)} |
| * <li>{@link #setOffline(Boolean)}, unless the configuration file specifies this |
| * </ul> |
| * |
| * <p>All files and directories in the source directory will be copied into the |
| * destination directory as is (recursively), except these, which will be |
| * ignored: |
| * <ul> |
| * <li>file or directory whose name starts with <tt>"docgen-"</tt> or |
| * <tt>"docgen."</tt>, or whose name is <tt>"docgen"</tt>. |
| * <li>file directly in the source directory (not in a sub-directory) |
| * with <tt>xml</tt> file extension |
| * <li>file or directory whose name looks like it's just backup, temporary, |
| * SVN-related or CVS-related entry. |
| * </ul> |
| * |
| * <p>The following files/directories are treated specially: |
| * <ul> |
| * <li><p><tt>book.xml</tt> or <tt>article.xml</tt> (<b>required</b>): the |
| * DocBook XML that we want to transform. It may contains XInclude-s so |
| * it doesn't have to store the whole book or article. |
| * |
| * <li><p><tt>docgen.cjson</tt> file (optional): |
| * contains Docgen settings. It uses an extended JSON syntax; see more |
| * <a href="#cjsonLanguage">later</a>. |
| * |
| * <p>Some notes about the settings in general: |
| * <ul> |
| * <li>Values that are of type "map" keep the order of entries as they were specified, |
| * where it matters.</li> |
| * <li>Values that describe link targets support not only usual URL-s, but also URL-s like |
| * {@code id:example} that links to the XML element with the given {@code xml:id} attribute, |
| * and {@code olink:example} that links to an URL specified in the {@code olinks} settings |
| * (see that later).</li> |
| * <li>All settings are optional, except where noted otherwise</li> |
| * </ul> |
| * |
| * <p>The supported settings are: |
| * <ul> |
| * <li> |
| * <p><tt>logo</tt> (map, required): The image used as the site logo. The map must be like: |
| * <tt>logo: { href: "http://example.com/", src: example.png, alt: "Example" }</tt>. |
| * <li> |
| * <p><tt>tabs</tt> (map): Defines the tabs on the top of the page. |
| * Each tab meant to represent an independently generated section of the web site. One of the tabs |
| * corresponds to the content that we are generating now, and that tab must be associated to the <tt>""</tt> |
| * target URL in this setting. Example:<tt><br> |
| * {<br> |
| * "Home": "" // Empty - We are here<br> |
| * "Manual": "olink:manual"<br> |
| * "Java API": "olink:api"<br> |
| * }</tt> |
| * <li><p><tt>secondaryTabs</tt> (object): An array of objects for the |
| * secondary tabs, like:<tt><br> |
| * {<br> |
| * "Contribute": { class: "icon-heart", href: "id:contribute" }<br> |
| * "Report a Bug": { ... }<br> |
| * ...<br> |
| * }</tt> |
| * <li><p><tt>ignoredFiles</tt> (object): The list of file name globs (can use: {@code *}, {@code ?} and |
| * {@code **}) of the files that won't be copyied over as static files. The are relative to the topmost |
| * source directory. |
| * <li><p><tt>socialLinks</tt> (object): An array of objects for the |
| * social links, like:<tt><br> |
| * {<br> |
| * "GitHub": { class: "icon-github", href: "https://github.com/example" },<br> |
| * "Twitter": { ... }<br> |
| * ...<br> |
| * }</tt> |
| * <li><p><tt>searchKey</tt> (string): A Google custom search key. If not |
| * present, the search box will not show. |
| * <li><p><tt>footerSiteMap</tt> (object): Defines the list of links to |
| * display in the footer as columns. Example:<tt><br> |
| * {<br> |
| * "Column Title 1": { "Row Title 1": "id:someTraget", "Row Title 2": "id:someTraget" }<br> |
| * "Column Title 2": { ... }<br> |
| * ...<br> |
| * }</tt> |
| * <li><p><tt>internalBookmarks</tt> (map): Specifies the first part |
| * of the book-mark link list that appears in the navigation bar. |
| * Associates labels with element ID-s (<tt>xml:id</tt> attribute |
| * values). |
| * <li><p><tt>externalBookmarks</tt> (map): Specifies the second part |
| * of the book-mark link list. Associates labels with arbitrary |
| * URL-s or paths. External bookmarks should |
| * be used to link to resources outside the documentation generated |
| * by this configuration. If the target resource is controlled by |
| * you, and so you can use the same set of tabs there as here, |
| * consider using <tt>tabs</tt> instead. |
| * <li><p><tt>offline</tt> (boolean): |
| * Specifies if the documentation will be generated for offline use. |
| * If it was already specified via {@link #setOffline(Boolean)}, then |
| * that has priority. If it wasn't specified via {@link #setOffline(Boolean)}, |
| * then it's mandatory to set. |
| * <li><p><tt>removeNodesWhenOnline</tt> (list of strings): |
| * The list of element {@code xml:id}-s of the XML elements that will be removed |
| * from the DocBook document if {@code offline} is {@code false}. This is just like |
| * if have deleted these elements (with their nested content) from the XML file. |
| * (This meant to be used to remove sections that are redundant when the book is the |
| * part of a bigger site.) |
| * <li><p><tt>seoMeta</tt> (map): |
| * Let you customize the metadata used by search engines for the selected pages. The page is selected with |
| * the map's key, which is either an {@code xml:id} or {@code "file:"} + output file name. The value of |
| * the key value pair is yet another map, which can have these keys (all optional): {@code description} |
| * to specify a short page summary, {@code title} to override the title of the Docbook element, and |
| * {@code fullTitle} to override the whole page title (shich used to be generated from the book title plus |
| * the current element's title). When you modify the title or full title, it will change the HTML |
| * {@code head/title} content, and some other elements in the {@code head} element, but not the |
| * title shown in the HTML body or in the Table of Contents. Example value:<tt><br> |
| * {<br> |
| * "file:index.html": {<br> |
| * "fullTitle": "Example Home Page"<br> |
| * "description": "Just an example."<br> |
| * }<br> |
| * "someElementId": {<br> |
| * "title": "Getting Started with Example"<br> |
| * }<br> |
| * }</tt> |
| * <li><p><tt>deployUrl</tt> (string): URL the page is deployed to (used |
| * for canonical URL-s) |
| * <li><p><tt>olinks</tt> (map): |
| * Maps <tt>olink</tt> <tt>targetdoc</tt> attribute values to |
| * actual URL-s. The target URL can also be an "olink:" or "id:" URL, and the result of resolving that can |
| * also be further such URL, and so on. |
| * <li><p><tt>validation</tt> (map): |
| * This is where you can configure the optional Docgen-specific |
| * DocBook validation restrictions. Accepted map entries are: |
| * <ul> |
| * <li><tt>programlistingsRequireRole</tt> (boolean): |
| * defaults to {@code false}. |
| * <li><tt>programlistingsRequireLanguage</tt> (boolean): |
| * defaults to {@code false}. |
| * <li><tt>outputFilesCanUseAutoID</tt> (boolean): |
| * defaults to {@code false}. |
| * <li><tt>maximumProgramlistingWidth</tt> (int): defaults to |
| * {@link Integer#MAX_VALUE}. The maximum number of characters |
| * per line in <tt>programlisting</tt>-s. |
| * </ul> |
| * |
| * <li><p><tt>contentDirectory</tt> (string): By default the Docgen |
| * configuration files and the files that store the actual book |
| * content (DocBook XML-s, images, etc.) are in the same directory, |
| * the so called source directory. By setting this setting, the last |
| * can be separated from the directory of the configuration files. |
| * If it's not an absolute path then it will be interpreted |
| * relatively to the source directory. |
| * |
| * <li><p><a name="setting_lowestFileElementRank"></a> |
| * <tt>lowestFileElementRank</tt> (string): The lowest document |
| * structure element "rank" for which an own output file will be |
| * created. Note that possibly not all such elements are shown in a |
| * given TOF (Table of Files) due to the <tt>maxTOFDisplayDepth</tt> |
| * or <tt>maxMainTOFDisplayDepth</tt> setting. |
| * |
| * <p>"rank" symbolizes how big structural unit the element stands |
| * for. The valid ranks are, from the lowest to the highest: |
| * {@code simplesect}, {@code section3}, {@code section2}, |
| * {@code section1}, {@code chapter}, {@code part}, |
| * {@code book}. |
| * If the name of an element is the same as one of the rank names |
| * then that will be its rank. For <tt>section</tt>-s, the number in |
| * the rank name tells how deeply the <tt>section</tt> is nested into |
| * other <tt>section</tt>-s (1 means a <tt>section</tt> that is not |
| * nested into any other <tt>section</tt>-s). For the other document |
| * structure elements (e.g. <tt>appendix</tt>, <tt>preface</tt>, |
| * etc.) the rank will be decided based on its surroundings. For |
| * example, <tt>book/appendix</tt>, if it has <tt>part</tt> siblings, |
| * will receive <tt>part</tt> rank, but if it has <tt>chapter</tt> |
| * siblings, then it will only receive <tt>chapter</tt> rank. Again |
| * the same kind of element, <tt>appendix</tt>, inside a |
| * <tt>chapter</tt> will only receive <tt>section1</tt> rank. |
| * It's good to know that nothing will receive <tt>chapter</tt> rank |
| * unless it's directly under a <tt>part</tt> element (not just a |
| * {@code part}-ranked element!) or <tt>book</tt> element. However, |
| * if the root element of the document is <tt>article</tt>, that |
| * will receive <tt>chapter</tt> rank. |
| * |
| * <p>Note that the content of some elements, like of |
| * <tt>preface</tt>-s, is kept in a single file regardless of this |
| * setting. |
| * |
| * <p>The default value is <tt>section1</tt>. |
| * |
| * <li><p><tt>lowestPageTOCElementRank</tt> (string): |
| * The lowest document structure element "rank" for which a |
| * "Page Contents" ToC entry will be created. |
| * |
| * <p>About "ranks" see <a href="#setting_lowestFileElementRank">the |
| * <tt>lowestFileElementRank</tt> setting</a>. |
| * |
| * <p>The default value is <tt>section3</tt>. |
| |
| * <li><p><tt>maxTOFDisplayDepth</tt> (int): In a given TOF |
| * (Table of Files) (because there can be multiple TOF-s, like there |
| * can be a book-level TOF, and then there can be chapter-level |
| * TOF-s), this is the nesting level until TOF entries are actually |
| * displayed. |
| * Depth level 0 is considered to by the level where the |
| * file-element of the HTML page which contains the TOF is. |
| * Defaults to {@link Integer#MAX_VALUE}. Must be at least {@code 1}. |
| * |
| * <li><p><tt>maxMainTOFDisplayDepth</tt> (int): Same as |
| * <tt>maxTOFDisplayDepth</tt>, but only applies to the TOF on the |
| * first (index) page. Defaults to the value of |
| * <tt>maxTOFDisplayDepth</tt>. |
| * |
| * <li><p><tt>numberedSections</tt> (boolean): Specifies if |
| * <tt>section</tt> element titles should be shown with numbering. |
| * This will result in titles like "2 something" "2.1 Something" or |
| * "2.1.3 Something", even "B.1 Something" (last is a |
| * <tt>section</tt> under Appendix B). |
| * |
| * <p>Note that within some elements, like inside <tt>preface</tt>-s, |
| * nothing has prefixes (labels) so this setting is ignored there. |
| * |
| * <li><p><tt>generateEclipseTOC</tt> (boolean): Sets whether an Eclipse |
| * ToC XML is generated for the generated HTML-s. Defaults to |
| * <tt>false</tt>. |
| * |
| * <li><p><tt>eclipse</tt> (map): |
| * Stores the settings of the Eclipse-ToC-generation. |
| * (Note that you still must turn that on with |
| * <tt>generateEclipseTOC</tt>; the mere presence |
| * of this setting will not do that.). |
| * Accepted map entries are: |
| * <ul> |
| * <li><tt>link_to</tt> (string): The value of |
| * <tt>toc.@link_to</tt> in the generated ToC file. If not |
| * specified, there will not be any <tt>link_to</tt> attribute. |
| * </ul> |
| * |
| * <li><p><tt>locale</tt> (string): The "nationality" used for |
| * lexical shorting, number formatting and such things. |
| * Defaults to <tt>"en_US"</tt>. |
| * |
| * <li><p><tt>timeZone</tt> (string): The time zone used for the |
| * date/time shown. Defaults to <tt>"GMT"</tt>. |
| * |
| * <li><p><tt>disableJavaScript</tt> (boolean): Disallow JavaScript in |
| * the generated pages. Defaults to <tt>false</tt> (i.e., JavaScript |
| * is allowed). The pages are more functional with JavaScript, but |
| * MSIE 6 and 7 (didn't tried 8) will show a security alert and block |
| * JavaScript if the page is opened from the local file-system (i.e., |
| * as <tt>file://...</tt> or <tt>C:\...</tt>, etc). So if the |
| * generated content is often read locally and the target audience is |
| * not IT-people (who know this thing very well, since even Javadoc |
| * output does this), you better set this to <tt>true</tt>. Note that |
| * even with JavaScript blocked by MSIE, the page will remain as |
| * functional as if you were generating it with |
| * <tt>disableJavaScript</tt> set to <tt>true</tt>, only the security |
| * warning is annoying. |
| * |
| * <li><p><tt>onlineTrackerHTML</tt> (string): The path of a HTML file |
| * whose content will be inserted before the <tt>body</tt> tag, unless |
| * <tt>offline</tt> was set to <tt>true</tt>. This is typically used to |
| * insert the Google Analytics <tt>script</tt> element. If this path is |
| * relative, it's relative to the source directory. |
| * |
| * <li><p><tt>cookineConsentScriptURL</tt> (string): The URL used as the <tt>src</tt> attribute of the |
| * EU cookie consent <tt>script</tt> tag. This is usually a CDN URL. |
| * |
| * <li><p><tt>showXXELogo</tt> (boolean): Specifies if an |
| * "Edited with XXE" logo should be shown on the generated pages. |
| * Defaults to <tt>false</tt>. |
| * |
| * <li><p><tt>copyrightHolder</tt> (String): Required. Used in the page footer copyright notice. |
| * <li><p><tt>copyrightHolderSite</tt> (String): Required. The homepage of the copyright holder. |
| * <li><p><tt>copyrightStartYear</tt> (String): Required. Used in the page footer copyright notice. |
| * <li><p><tt>copyrightSuffix</tt> (String): Optional. Appended after the generated copyright text. |
| * <li><p><tt>copyrightCommentFile</tt> (String): The path of a HTML file to the text used inside |
| * the output files as copyright header comment. If this path is relative, it's relative to the source |
| * directory. Currently, the copyright comment is only inserted if the {@code offline} mode is {@code true}. |
| * That's because at ASF currently only the documentation files that are part of the released archive need |
| * these comments. |
| * |
| * <li><p><tt>sideTOCLogos</tt> (list of maps): Specifies the logos that should be shown under the |
| * Table of Contents shown on the side of each page. This is usually used to show the logos of sponsors. |
| * The logo images should as wide as the ToC, and will be listed vertically. The maps in the list should |
| * follow the same format as the <tt>logo</tt> setting. |
| * |
| * </ul> |
| * |
| * <li><p><tt>docgen-templates</tt> directory: |
| * The templates here will have priority over the ones in the |
| * {@code org.freemarker.docgen.templates} package. |
| * This is mostly used for overriding <tt>customizations.ftlh</tt>; |
| * that FTL is <tt>#import</tt>-ed at the beginning of all |
| * template files, and searched first for the |
| * <tt>#visit</tt>/<tt>#recurse</tt> calls. |
| * </ul> |
| * |
| * |
| * <p><b><font size="+1"><a name="cjsonLanguage"></a> |
| * The CJSON language |
| * </font></b></p> |
| * |
| * <p>It's JSON extended with some features that make it more convenient for |
| * configuration files: |
| * <ul> |
| * <li>String literals whose value only contains letters (UNICODE), digits |
| * (UNICODE) and characters {@code '.'}, {@code '_'}, {@code '$'}, |
| * {@code '@'}, and {@code '-'}, but don't start with |
| * characters 0-9 or is {@code true} or {@code false}, need not be |
| * quoted. Thus instead of |
| * <tt>{"name": "Big Joe", "color": "red"}</tt> you can just |
| * write <tt>{name: "Big Joe", color: red}</tt>. (There are no |
| * variable references in CJSON.) |
| * <li>In key-value pairs the value defaults to {@code true}. Like, instead |
| * of <tt>{showLogo: true}</tt> you can just write <tt>{showLogo}</tt>. |
| * <li>You can omit the commas that otherwise would be at the end of the line. |
| * <li>JavaScript comments are supported (<tt>/* ... *<!-- -->/</tt> and |
| * <tt>// ...</tt>) |
| * <li>If a file is expected to contain a JSON object, like most configuration |
| * files are, putting the whole thing between <tt>{</tt> and <tt>}</tt> is |
| * optional. |
| * <li>Maps remember the order in which the entries were specified in the |
| * expression. The consumer of the configuration file will not utilize |
| * this for most settings anyway, but for certain kind of settings it's |
| * just more intuitive than getting the entries in a some random order. |
| * <li>A comma may be used after the last item of a list or map. |
| * <li>Supports FTL raw string literals (e.g. {@code r"C:\Windows\System32"}). |
| * <li>Supports function calls (e.g. {@code f(1, 2)}), although it's up to the |
| * consumer to resolve them; the CJSON language itself doesn't define any |
| * functions. |
| * </ul> |
| * |
| * <p>When CJSON is stored in a file, the file extension should be |
| * <tt>cjson</tt> and UTF-8 charset should be is used. However, the charset can |
| * be overridden with a initial |
| * <tt>// charset: <i>charsetName</i></tt> comment [*]. |
| * Initial BOM is silently ignored. |
| * |
| * <blockquote> |
| * <p>* The comment is considered to be a charset override only if when it's |
| * decoded with ISO-8859-1 it stands that: |
| * <ul> |
| * <li>Apart from white-space (and an initial BOM) it's the first thing in |
| * the file. |
| * <li>It's a <tt>//</tt> comment, not a <tt>/* ... *<!-- -->/</tt> comment. |
| * <li>Ignoring white-space, the first word inside the comment is |
| * <tt>charset</tt> or <tt>encoding</tt> (they are equivalent). That's |
| * followed by optional whitespace, then a colon, then optional |
| * whitespace again. Then a non-whitespace character (the first letter of |
| * the charset name). At this point the comment already counts as a |
| * charset override. Starting from there, until the end of the line or |
| * of the file (whichever comes first) all kind of characters can occur, |
| * and they will all belong to the charset name (which will be |
| * interpreted after trimming surrounding whitespace). |
| * </ul> |
| * </blockquote> |
| */ |
| public final class Transform { |
| |
| // ------------------------------------------------------------------------- |
| // Constants: |
| |
| static final String FILE_BOOK = "book.xml"; |
| static final String FILE_ARTICLE = "article.xml"; |
| static final String FILE_SETTINGS = "docgen.cjson"; |
| /** Used for the Table of Contents file when a different node was marked to be the index.html. */ |
| static final String FILE_TOC_HTML = "toc.html"; |
| static final String FILE_DETAILED_TOC_HTML = "detailed-toc.html"; |
| static final String FILE_INDEX_HTML = "index.html"; |
| static final String FILE_SEARCH_RESULTS_HTML = "search-results.html"; |
| static final String FILE_TOC_JSON_TEMPLATE = "toc-json.ftl"; |
| static final String FILE_TOC_JSON_OUTPUT = "toc.js"; |
| static final String FILE_ECLIPSE_TOC_TEMPLATE = "eclipse-toc.ftlx"; |
| static final String FILE_ECLIPSE_TOC_OUTPUT = "eclipse-toc.xml"; |
| static final String DIR_TEMPLATES = "docgen-templates"; |
| |
| static final String FILE_SITEMAP_XML_TEMPLATE = "sitemap.ftlx"; |
| static final String FILE_SITEMAP_XML_OUTPUT = "sitemap.xml"; |
| |
| static final String SETTING_IGNORED_FILES = "ignoredFiles"; |
| static final String SETTING_VALIDATION = "validation"; |
| static final String SETTING_OFFLINE = "offline"; |
| static final String SETTING_SIMPLE_NAVIGATION_MODE = "simpleNavigationMode"; |
| static final String SETTING_DEPLOY_URL = "deployUrl"; |
| static final String SETTING_ONLINE_TRACKER_HTML = "onlineTrackerHTML"; |
| static final String SETTING_COOKIE_CONSENT_SCRIPT_URL = "cookieConsentScriptURL"; |
| static final String SETTING_REMOVE_NODES_WHEN_ONLINE = "removeNodesWhenOnline"; |
| static final String SETTING_INTERNAL_BOOKMARKS = "internalBookmarks"; |
| static final String SETTING_EXTERNAL_BOOKMARKS = "externalBookmarks"; |
| static final String SETTING_COPYRIGHT_HOLDER = "copyrightHolder"; |
| static final String SETTING_COPYRIGHT_HOLDER_SITE = "copyrightHolderSite"; |
| static final String SETTING_COPYRIGHT_START_YEAR = "copyrightStartYear"; |
| static final String SETTING_COPYRIGHT_SUFFIX = "copyrightSuffix"; |
| static final String SETTING_COPYRIGHT_COMMENT_FILE = "copyrightCommentFile"; |
| static final String SETTING_SEO_META = "seoMeta"; |
| static final String SETTING_LOGO = "logo"; |
| static final String SETTING_LOGO_KEY_SRC = "src"; |
| static final String SETTING_LOGO_KEY_ALT = "alt"; |
| static final String SETTING_LOGO_KEY_HREF = "href"; |
| static final String SETTING_SIDE_TOC_LOGOS = "sideTOCLogos"; |
| static final String SETTING_TABS = "tabs"; |
| static final String SETTING_SECONDARY_TABS = "secondaryTabs"; |
| static final String SETTING_SOCIAL_LINKS = "socialLinks"; |
| static final String SETTING_FOOTER_SITEMAP = "footerSiteMap"; |
| static final String SETTING_OLINKS = "olinks"; |
| static final String SETTING_ECLIPSE = "eclipse"; |
| static final String SETTING_SHOW_EDITORAL_NOTES = "showEditoralNotes"; |
| static final String SETTING_GENERATE_ECLIPSE_TOC = "generateEclipseTOC"; |
| static final String SETTING_SHOW_XXE_LOGO = "showXXELogo"; |
| static final String SETTING_SEARCH_KEY = "searchKey"; |
| static final String SETTING_DISABLE_JAVASCRIPT = "disableJavaScript"; |
| static final String SETTING_TIME_ZONE = "timeZone"; |
| static final String SETTING_LOCALE = "locale"; |
| static final String SETTING_CONTENT_DIRECTORY = "contentDirectory"; |
| static final String SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK |
| = "lowestPageTOCElementRank"; |
| static final String SETTING_LOWEST_FILE_ELEMENT_RANK |
| = "lowestFileElementRank"; |
| static final String SETTING_MAX_TOF_DISPLAY_DEPTH = "maxTOFDisplayDepth"; |
| static final String SETTING_MAX_MAIN_TOF_DISPLAY_DEPTH |
| = "maxMainTOFDisplayDepth"; |
| static final String SETTING_NUMBERED_SECTIONS = "numberedSections"; |
| |
| static final String SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE |
| = "programlistingsRequireRole"; |
| static final String SETTING_VALIDATION_PROGRAMLISTINGS_REQ_LANG |
| = "programlistingsRequireLanguage"; |
| static final String SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID |
| = "outputFilesCanUseAutoID"; |
| static final String SETTING_VALIDATION_MAXIMUM_PROGRAMLISTING_WIDTH |
| = "maximumProgramlistingWidth"; |
| static final String SETTING_ECLIPSE_LINK_TO = "link_to"; |
| |
| static final String SETTING_SEO_META_KEY_TITLE = "title"; |
| static final String SETTING_SEO_META_KEY_FULL_TITLE = "fullTitle"; |
| static final String SETTING_SEO_META_KEY_DESCRIPTION = "description"; |
| static final Set<String> SETTING_SEO_META_KEYS; |
| static { |
| SETTING_SEO_META_KEYS = new LinkedHashSet<>(); |
| SETTING_SEO_META_KEYS.add(SETTING_SEO_META_KEY_TITLE); |
| SETTING_SEO_META_KEYS.add(SETTING_SEO_META_KEY_FULL_TITLE); |
| SETTING_SEO_META_KEYS.add(SETTING_SEO_META_KEY_DESCRIPTION); |
| } |
| |
| static final String COMMON_LINK_KEY_CLASS = "class"; |
| static final String COMMON_LINK_KEY_HREF = "href"; |
| static final Set<String> COMMON_LINK_KEYS; |
| static { |
| COMMON_LINK_KEYS = new LinkedHashSet<>(); |
| COMMON_LINK_KEYS.add(COMMON_LINK_KEY_CLASS); |
| COMMON_LINK_KEYS.add(COMMON_LINK_KEY_HREF); |
| } |
| |
| private static final String VAR_OFFLINE |
| = SETTING_OFFLINE; |
| private static final String VAR_SIMPLE_NAVIGATION_MODE |
| = SETTING_SIMPLE_NAVIGATION_MODE; |
| private static final String VAR_DEPLOY_URL |
| = SETTING_DEPLOY_URL; |
| private static final String VAR_ONLINE_TRACKER_HTML |
| = SETTING_ONLINE_TRACKER_HTML; |
| private static final String VAR_COOKIE_CONSENT_SCRIPT_URL = SETTING_COOKIE_CONSENT_SCRIPT_URL; |
| private static final String VAR_COPYRIGHT_COMMENT = "copyrightComment"; |
| private static final String VAR_COPYRIGHT_JAVA_COMMENT = "copyrightJavaComment"; |
| private static final String VAR_SHOW_EDITORAL_NOTES |
| = "showEditoralNotes"; |
| private static final String VAR_TRANSFORM_START_TIME |
| = "transformStartTime"; |
| private static final String VAR_SHOW_XXE_LOGO |
| = SETTING_SHOW_XXE_LOGO; |
| private static final String VAR_SEARCH_KEY |
| = SETTING_SEARCH_KEY; |
| private static final String VAR_DISABLE_JAVASCRIPT |
| = SETTING_DISABLE_JAVASCRIPT; |
| private static final String VAR_ECLIPSE_LINK_TO = SETTING_ECLIPSE_LINK_TO; |
| private static final String VAR_INTERNAL_BOOKMARDS |
| = SETTING_INTERNAL_BOOKMARKS; |
| private static final String VAR_EXTERNAL_BOOKMARDS |
| = SETTING_EXTERNAL_BOOKMARKS; |
| private static final String VAR_LOGO = SETTING_LOGO; |
| private static final String VAR_SIDE_TOC_LOGOS = SETTING_SIDE_TOC_LOGOS; |
| private static final String VAR_COPYRIGHT_HOLDER = SETTING_COPYRIGHT_HOLDER; |
| private static final String VAR_COPYRIGHT_HOLDER_SITE = SETTING_COPYRIGHT_HOLDER_SITE; |
| private static final String VAR_COPYRIGHT_START_YEAR = SETTING_COPYRIGHT_START_YEAR; |
| private static final String VAR_COPYRIGHT_SUFFIX = SETTING_COPYRIGHT_SUFFIX; |
| private static final String VAR_SEO_META_TITLE_OVERRIDE = "seoMetaTitleOverride"; |
| private static final String VAR_SEO_META_FULL_TITLE_OVERRIDE = "seoMetaFullTitleOverride"; |
| private static final String VAR_SEO_META_DESCRIPTION = "seoMetaDescription"; |
| private static final String VAR_TABS = SETTING_TABS; |
| private static final String VAR_SECONDARY_TABS = SETTING_SECONDARY_TABS; |
| private static final String VAR_SOCIAL_LINKS = SETTING_SOCIAL_LINKS; |
| private static final String VAR_FOOTER_SITEMAP = SETTING_FOOTER_SITEMAP; |
| private static final String VAR_OLINKS |
| = SETTING_OLINKS; |
| private static final String VAR_TOC_DISPLAY_DEPTH |
| = SETTING_MAX_TOF_DISPLAY_DEPTH; |
| private static final String VAR_NUMBERED_SECTIONS |
| = SETTING_NUMBERED_SECTIONS; |
| private static final String VAR_INDEX_ENTRIES |
| = "indexEntries"; |
| private static final String VAR_PAGE_TYPE = "pageType"; |
| private static final String VAR_ALTERNATIVE_TOC_LINK |
| = "alternativeTOCLink"; |
| private static final String VAR_ALTERNATIVE_TOC_LABEL |
| = "alternativeTOCLabel"; |
| private static final String VAR_PARENT_FILE_ELEMENT = "parentFileElement"; |
| private static final String VAR_NEXT_FILE_ELEMENT = "nextFileElement"; |
| private static final String VAR_PREVIOUS_FILE_ELEMENT |
| = "previousFileElement"; |
| private static final String VAR_ROOT_ELEMENT = "rootElement"; |
| private static final String VAR_SHOW_NAVIGATION_BAR = "showNavigationBar"; |
| private static final String VAR_SHOW_BREADCRUMB = "showBreadCrumb"; |
| |
| private static final String VAR_JSON_TOC_ROOT = "tocRoot"; |
| |
| private static final String PAGE_TYPE_DETAILED_TOC = "docgen:detailed_toc"; |
| private static final String PAGE_TYPE_SEARCH_RESULTS = "docgen:search_results"; |
| |
| private static final String OLINK_SCHEMA_START = "olink:"; |
| private static final String ID_SCHEMA_START = "id:"; |
| |
| private static final Charset UTF_8 = Charset.forName("UTF-8"); |
| |
| static final String SYSPROP_GENERATION_TIME = "docgen.generationTime"; |
| |
| // Docgen-specific XML attributes (added during DOM-tree postediting): |
| |
| /** |
| * Marks an element for which a separate file is created; attached to |
| * document structure elements, value is always {@code "true"}. |
| */ |
| private static final String A_DOCGEN_FILE_ELEMENT = "docgen_file_element"; |
| |
| /** |
| * Marks an element for which a page ToC ("Page Contents") line is shown; |
| * attached to document structure elements, it's value is always |
| * {@code "true"}. |
| */ |
| private static final String A_DOCGEN_PAGE_TOC_ELEMENT |
| = "docgen_page_toc_element"; |
| |
| /** |
| * Marks and element that is shown in the <em>detailed</em> main ToC; |
| * attached to document structure elements, it's value is always |
| * {@code "true"}. |
| */ |
| private static final String A_DOCGEN_DETAILED_TOC_ELEMENT |
| = "docgen_detailed_toc_element"; |
| |
| /** |
| * The top-level document-structure element is marked with this; |
| * it's value is always {@code "true"}. |
| */ |
| private static final String A_DOCGEN_ROOT_ELEMENT = "docgen_root_element"; |
| |
| /** |
| * The numbering or letter or whatever that is shown before the tile, such |
| * as "2.4" or "IV"; attached to document structure elements that use a |
| * title prefix. |
| */ |
| private static final String A_DOCGEN_TITLE_PREFIX = "docgen_title_prefix"; |
| |
| /** |
| * The integer ordinal of the document structure element within its own ToC |
| * level, counting all kind of preceding document structure siblings; |
| * attached to the document structure element. |
| * |
| * @see #A_DOCGEN_NUMBERING |
| */ |
| private static final String A_DOCGEN_UNITED_NUMBERING |
| = "docgen_united_numbering"; |
| |
| /** |
| * Describes how "big" a title should be; attached to the document structure |
| * element (no to the title element). For the possible values see the |
| * {@code AV_DOCGEN_TITLE_RANK_...} constants. For even more information see |
| * {@link #preprocessDOM_addRanks(Document)}. |
| */ |
| private static final String A_DOCGEN_RANK = "docgen_rank"; |
| |
| /** An element for which it's not possible to create a link. */ |
| private static final String A_DOCGEN_NOT_ADDRESSABLE = "docgen_not_addressable"; |
| |
| private static final String AV_INDEX_ROLE = "index.html"; |
| |
| /** |
| * This is how automatically added id attribute values start. |
| */ |
| static final String AUTO_ID_PREFIX = "autoid_"; |
| |
| static final String DOCGEN_ID_PREFIX = "docgen_"; |
| |
| /** Elements for which an id attribute automatically added if missing */ |
| private static final Set<String> GUARANTEED_ID_ELEMENTS; |
| static { |
| Set<String> idAttElems = new HashSet<String>(); |
| |
| for (String elemName : DOCUMENT_STRUCTURE_ELEMENTS) { |
| idAttElems.add(elemName); |
| } |
| |
| idAttElems.add(E_GLOSSARY); |
| idAttElems.add(E_GLOSSENTRY); |
| |
| GUARANTEED_ID_ELEMENTS = Collections.unmodifiableSet(idAttElems); |
| } |
| |
| /** |
| * Elements whose children will go into a single output file regardless |
| * of the element ranks, and whose children never use title prefixes |
| * (labels). |
| */ |
| private static final Set<String> PREFACE_LIKE_ELEMENTS; |
| static { |
| Set<String> sinlgeFileElems = new HashSet<String>(); |
| |
| sinlgeFileElems.add(E_PREFACE); |
| |
| PREFACE_LIKE_ELEMENTS = Collections.unmodifiableSet(sinlgeFileElems); |
| } |
| |
| private static final String XMLNS_DOCGEN = "http://freemarker.org/docgen"; |
| private static final String E_SEARCHRESULTS = "searchresults"; |
| private static final String SEARCH_RESULTS_PAGE_TITLE = "Search results"; |
| private static final String SEARCH_RESULTS_ELEMENT_ID = "searchresults"; |
| |
| // ------------------------------------------------------------------------- |
| // Settings: |
| |
| private File destDir; |
| |
| private File srcDir; |
| |
| private File contentDir; |
| |
| private List<Pattern> ignoredFilePathPatterns = new ArrayList<>(); |
| |
| private Boolean offline; |
| |
| private String deployUrl; |
| |
| private String onlineTrackerHTML; |
| private String cookieConstentScriptURL; |
| |
| private Set<String> removeNodesWhenOnline; |
| |
| /** Element types for which a new output file is created */ |
| private DocumentStructureRank lowestFileElemenRank |
| = DocumentStructureRank.SECTION1; |
| |
| private DocumentStructureRank lowestPageTOCElemenRank |
| = DocumentStructureRank.SECTION3; |
| |
| private int maxTOFDisplayDepth = Integer.MAX_VALUE; |
| |
| private int maxMainTOFDisplayDepth; // 0 indicates "not set"; |
| |
| private boolean numberedSections; |
| |
| private boolean generateEclipseTOC; |
| |
| private boolean simpleNavigationMode; |
| |
| private boolean showEditoralNotes; |
| |
| private boolean showXXELogo; |
| |
| private String searchKey; |
| |
| private boolean disableJavaScript; |
| |
| private boolean validate = true; |
| |
| private Locale locale = Locale.US; |
| |
| private TimeZone timeZone = TimeZone.getTimeZone("GMT"); |
| |
| private boolean printProgress; |
| |
| private LinkedHashMap<String, String> internalBookmarks = new LinkedHashMap<String, String>(); |
| private LinkedHashMap<String, String> externalBookmarks = new LinkedHashMap<>(); |
| private Map<String, Map<String, String>> footerSiteMap; |
| |
| private LinkedHashMap<String, String> tabs = new LinkedHashMap<>(); |
| |
| private Map<String, Map<String, String>> secondaryTabs; |
| private Map<String, Map<String, String>> socialLinks; |
| |
| private Logo logo; |
| |
| private List<Logo> sideTOCLogos; |
| |
| private String copyrightHolder; |
| private String copyrightHolderSite; |
| private String copyrightSuffix; |
| private Integer copyrightStartYear; |
| private String copyrightComment; |
| private String copyrightJavaComment; |
| |
| private Map<String, Map<String, String>> seoMeta; |
| |
| private DocgenValidationOptions validationOps |
| = new DocgenValidationOptions(); |
| |
| // ------------------------------------------------------------------------- |
| // Global transformation state: |
| |
| private boolean executed; |
| |
| private Map<String, String> olinks = new HashMap<String, String>(); |
| private Map<String, List<NodeModel>> primaryIndexTermLookup; |
| private Map<String, SortedMap<String, List<NodeModel>>> |
| secondaryIndexTermLookup; |
| private Map<String, Element> elementsById; |
| private List<TOCNode> tocNodes; |
| private List<String> indexEntries; |
| private Configuration fmConfig; |
| |
| // ------------------------------------------------------------------------- |
| // Output-file-specific state: |
| |
| private TOCNode currentFileTOCNode; |
| |
| // ------------------------------------------------------------------------- |
| // Misc. fields: |
| |
| private DocgenLogger logger = new DocgenLogger() { |
| |
| public void info(String message) { |
| if (printProgress) { |
| System.out.println(message); |
| } |
| } |
| |
| public void warning(String message) { |
| if (printProgress) { |
| System.out.println("Warning:" + message); |
| } |
| } |
| }; |
| |
| // ------------------------------------------------------------------------- |
| // Methods: |
| |
| /** |
| * Loads the source XML and generates the output in the destination |
| * directory. Don't forget to set JavaBean properties first. |
| * |
| * @throws DocgenException If a docgen-specific error occurs |
| * @throws IOException If a file or other resource is missing or otherwise |
| * can't be read/written. |
| * @throws SAXException If the XML is not well-formed and valid, or the |
| * SAX XML parsing has other problems. |
| */ |
| public void execute() |
| throws DocgenException, IOException, SAXException { |
| if (executed) { |
| throw new DocgenException( |
| "This transformation was alrady executed; " |
| + "use a new " + Transform.class.getName() + "."); |
| } |
| executed = true; |
| |
| // Check Java Bean properties: |
| |
| if (srcDir == null) { |
| throw new DocgenException( |
| "The source directory (the DocBook XML) wasn't specified."); |
| } |
| if (!srcDir.isDirectory()) { |
| throw new IOException( |
| "Source directory doesn't exist: " |
| + srcDir.getAbsolutePath()); |
| } |
| |
| if (destDir == null) { |
| throw new DocgenException( |
| "The destination directory wasn't specified."); |
| } |
| // Note: This directory will be created automatically if missing. |
| |
| // Load configuration file: |
| |
| File templatesDir = null; |
| String eclipseLinkTo = null; |
| |
| File cfgFile = new File(srcDir, FILE_SETTINGS); |
| if (cfgFile.exists()) { |
| Map<String, Object> cfg; |
| try { |
| cfg = CJSONInterpreter.evalAsMap(cfgFile); |
| } catch (CJSONInterpreter.EvaluationException e) { |
| throw new DocgenException(e.getMessage(), |
| e.getCause()); |
| } |
| |
| for (Entry<String, Object> cfgEnt : cfg.entrySet()) { |
| final String settingName = cfgEnt.getKey(); |
| final Object settingValue = cfgEnt.getValue(); |
| |
| if (settingName.equals(SETTING_IGNORED_FILES)) { |
| List<String> patterns = castSettingToStringList(cfgFile, settingName, settingValue); |
| for (String pattern : patterns) { |
| ignoredFilePathPatterns.add(FileUtil.globToRegexp(pattern)); |
| } |
| } else if (settingName.equals(SETTING_OLINKS)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String name = ent.getKey(); |
| String target = castSettingValueMapValueToString( |
| cfgFile, settingName, ent.getValue()); |
| olinks.put(name, target); |
| } |
| } else if (settingName.equals(SETTING_INTERNAL_BOOKMARKS)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String name = ent.getKey(); |
| String target = castSettingValueMapValueToString( |
| cfgFile, settingName, ent.getValue()); |
| internalBookmarks.put(name, target); |
| } |
| // Book-mark targets will be checked later, when the XML |
| // document is already loaded. |
| } else if (settingName.equals(SETTING_EXTERNAL_BOOKMARKS)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String name = ent.getKey(); |
| String target = castSettingValueMapValueToString( |
| cfgFile, settingName, ent.getValue()); |
| externalBookmarks.put(name, target); |
| } |
| } else if (settingName.equals(SETTING_LOGO)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| logo = castMapToLogo(cfgFile, settingName, m); |
| } else if (settingName.equals(SETTING_SIDE_TOC_LOGOS)) { |
| List<Map<String, Object>> listOfMaps |
| = castSettingToListOfMapsWithStringKeys(cfgFile, settingName, settingValue); |
| sideTOCLogos = new ArrayList<>(); |
| for (int i = 0; i < listOfMaps.size(); i++) { |
| Map<String, Object> map = listOfMaps.get(i); |
| sideTOCLogos.add(castMapToLogo(cfgFile, settingName + "[" + i + "]", map)); |
| } |
| } else if (settingName.equals(SETTING_COPYRIGHT_HOLDER)) { |
| copyrightHolder = castSettingToString(cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_COPYRIGHT_HOLDER_SITE)) { |
| copyrightHolderSite = castSettingToString(cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_COPYRIGHT_START_YEAR)) { |
| copyrightStartYear = castSettingToInt(cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_COPYRIGHT_SUFFIX)) { |
| copyrightSuffix = castSettingToString(cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_COPYRIGHT_COMMENT_FILE)) { |
| copyrightComment = StringUtil.chomp(getFileContentForSetting(cfgFile, settingName, settingValue)); |
| String eol = TextUtil.detectEOL(copyrightComment, "\n"); |
| StringBuilder sb = new StringBuilder("/*").append(eol); |
| new BufferedReader(new StringReader(copyrightComment)).lines() |
| .forEach(s -> sb.append(" * ").append(s).append(eol)); |
| sb.append(" */"); |
| copyrightJavaComment = sb.toString(); |
| } else if (settingName.equals(SETTING_SEO_META)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| seoMeta = new LinkedHashMap<>(); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String k = ent.getKey(); |
| Map<String, String> v = castSettingValueMapValueToMapOfStringString( |
| cfgFile, settingName, ent.getValue(), |
| null, SETTING_SEO_META_KEYS); |
| seoMeta.put(k, v); |
| } |
| } else if (settingName.equals(SETTING_TABS)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String k = ent.getKey(); |
| String v = castSettingValueMapValueToString(cfgFile, settingName, ent.getValue()); |
| tabs.put(k, v); |
| } |
| } else if (settingName.equals(SETTING_SECONDARY_TABS)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| secondaryTabs = new LinkedHashMap<>(); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String k = ent.getKey(); |
| Map<String, String> v = castSettingValueMapValueToMapOfStringString( |
| cfgFile, settingName, ent.getValue(), |
| COMMON_LINK_KEYS, null); |
| secondaryTabs.put(k, v); |
| } |
| } else if (settingName.equals(SETTING_SOCIAL_LINKS)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| socialLinks = new LinkedHashMap<>(); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String entName = ent.getKey(); |
| Map<String, String> entValue = castSettingValueMapValueToMapOfStringString( |
| cfgFile, settingName, ent.getValue(), |
| COMMON_LINK_KEYS, null); |
| socialLinks.put(entName, entValue); |
| } |
| } else if (settingName.equals(SETTING_FOOTER_SITEMAP)) { |
| // TODO Check value in more details |
| footerSiteMap = (Map) castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| }else if (settingName.equals(SETTING_VALIDATION)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, SETTING_VALIDATION, settingValue); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String name = ent.getKey(); |
| if (name.equals( |
| SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE)) { |
| validationOps.setProgramlistingRequiresRole( |
| caseSettingToBoolean( |
| cfgFile, |
| settingName + "." + name, |
| ent.getValue())); |
| } else if (name.equals( |
| SETTING_VALIDATION_PROGRAMLISTINGS_REQ_LANG)) { |
| validationOps.setProgramlistingRequiresLanguage( |
| caseSettingToBoolean( |
| cfgFile, |
| settingName + "." + name, |
| ent.getValue())); |
| } else if (name.equals( |
| SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID) |
| ) { |
| validationOps.setOutputFilesCanUseAutoID( |
| caseSettingToBoolean( |
| cfgFile, |
| settingName + "." + name, |
| ent.getValue())); |
| } else if (name.equals( |
| SETTING_VALIDATION_MAXIMUM_PROGRAMLISTING_WIDTH) |
| ) { |
| validationOps.setMaximumProgramlistingWidth( |
| castSettingToInt( |
| cfgFile, |
| settingName + "." + name, |
| ent.getValue())); |
| } else { |
| throw newCfgFileException( |
| cfgFile, SETTING_VALIDATION, |
| "Unknown validation option: " + name); |
| } |
| } |
| } else if (settingName.equals(SETTING_OFFLINE)) { |
| if (offline == null) { // Ignore if the caller has already set this |
| offline = caseSettingToBoolean(cfgFile, settingName, settingValue); |
| } |
| } else if (settingName.equals(SETTING_SIMPLE_NAVIGATION_MODE)) { |
| simpleNavigationMode = caseSettingToBoolean(cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_DEPLOY_URL)) { |
| deployUrl = castSettingToString(cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_ONLINE_TRACKER_HTML)) { |
| onlineTrackerHTML = getFileContentForSetting(cfgFile, settingName, settingValue); |
| if (onlineTrackerHTML.startsWith("<!--")) { |
| int commentEnd = onlineTrackerHTML.indexOf("-->"); |
| if (commentEnd != -1) { |
| commentEnd += 3; |
| String comment = onlineTrackerHTML.substring(0, commentEnd); |
| if (comment.contains("copyright") || comment.contains("Copyright")) { |
| onlineTrackerHTML = onlineTrackerHTML.substring(commentEnd); |
| } |
| } |
| } |
| String eol = TextUtil.detectEOL(onlineTrackerHTML, "\n"); |
| onlineTrackerHTML = onlineTrackerHTML.trim(); |
| onlineTrackerHTML += eol; |
| } else if (settingName.equals(SETTING_COOKIE_CONSENT_SCRIPT_URL)) { |
| cookieConstentScriptURL = castSettingToString(cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_REMOVE_NODES_WHEN_ONLINE)) { |
| removeNodesWhenOnline = Collections.unmodifiableSet(new HashSet<String>( |
| castSettingToStringList(cfgFile, settingName, settingValue))); |
| } else if (settingName.equals(SETTING_ECLIPSE)) { |
| Map<String, Object> m = castSettingToMapWithStringKeys( |
| cfgFile, settingName, settingValue); |
| for (Entry<String, Object> ent : m.entrySet()) { |
| String name = ent.getKey(); |
| if (name.equals(SETTING_ECLIPSE_LINK_TO)) { |
| String value = castSettingToString( |
| cfgFile, |
| settingName + "." + name, |
| ent.getValue()); |
| eclipseLinkTo = value; |
| } else { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Unknown Eclipse option: " + name); |
| } |
| } |
| } else if (settingName.equals(SETTING_LOCALE)) { |
| String s = castSettingToString( |
| cfgFile, settingName, settingValue); |
| locale = StringUtil.deduceLocale(s); |
| } else if (settingName.equals(SETTING_TIME_ZONE)) { |
| String s = castSettingToString( |
| cfgFile, settingName, settingValue); |
| timeZone = TimeZone.getTimeZone(s); |
| } else if (settingName.equals(SETTING_GENERATE_ECLIPSE_TOC)) { |
| generateEclipseTOC = caseSettingToBoolean( |
| cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_SHOW_EDITORAL_NOTES)) { |
| showEditoralNotes = caseSettingToBoolean( |
| cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_SHOW_XXE_LOGO)) { |
| showXXELogo = caseSettingToBoolean( |
| cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_SEARCH_KEY)) { |
| searchKey = castSettingToString( |
| cfgFile, settingName, settingValue); |
| }else if (settingName.equals(SETTING_DISABLE_JAVASCRIPT)) { |
| disableJavaScript = caseSettingToBoolean( |
| cfgFile, settingName, settingValue); |
| } else if (settingName.equals(SETTING_CONTENT_DIRECTORY)) { |
| String s = castSettingToString( |
| cfgFile, settingName, settingValue); |
| contentDir = new File(srcDir, s); |
| if (!contentDir.isDirectory()) { |
| throw newCfgFileException(cfgFile, settingName, |
| "It's not an existing directory: " |
| + contentDir.getAbsolutePath()); |
| } |
| } else if (settingName.equals(SETTING_LOWEST_FILE_ELEMENT_RANK) |
| || settingName.equals( |
| SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) { |
| DocumentStructureRank rank; |
| String strRank = castSettingToString( |
| cfgFile, settingName, settingValue); |
| try { |
| rank = DocumentStructureRank.valueOf( |
| strRank.toUpperCase()); |
| } catch (IllegalArgumentException e) { |
| String msg; |
| if (strRank.equalsIgnoreCase("article")) { |
| msg = "\"article\" is not a rank, since articles " |
| + "can have various ranks depending on their " |
| + "context. (Hint: if the article is the " |
| + "top-level element then it has \"chapter\" " |
| + "rank.)"; |
| } else { |
| msg = "Unknown rank: " + strRank; |
| } |
| throw newCfgFileException(cfgFile, settingName, |
| msg); |
| } |
| |
| if (settingName.equals( |
| SETTING_LOWEST_FILE_ELEMENT_RANK)) { |
| lowestFileElemenRank = rank; |
| } else if (settingName.equals( |
| SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) { |
| lowestPageTOCElemenRank = rank; |
| } else { |
| throw new BugException("Unexpected setting name."); |
| } |
| } else if (settingName.equals(SETTING_MAX_TOF_DISPLAY_DEPTH)) { |
| maxTOFDisplayDepth = castSettingToInt( |
| cfgFile, settingName, settingValue); |
| if (maxTOFDisplayDepth < 1) { |
| throw newCfgFileException(cfgFile, settingName, |
| "Value must be at least 1."); |
| } |
| } else if (settingName.equals( |
| SETTING_MAX_MAIN_TOF_DISPLAY_DEPTH)) { |
| maxMainTOFDisplayDepth = castSettingToInt( |
| cfgFile, settingName, settingValue); |
| if (maxTOFDisplayDepth < 1) { |
| throw newCfgFileException(cfgFile, settingName, |
| "Value must be at least 1."); |
| } |
| } else if (settingName.equals(SETTING_NUMBERED_SECTIONS)) { |
| numberedSections = caseSettingToBoolean( |
| cfgFile, settingName, settingValue); |
| } else { |
| throw newCfgFileException(cfgFile, "Unknown setting: \"" |
| + settingName |
| + "\". (Hint: See the list of available " |
| + "settings in the Java API documentation of " |
| + Transform.class.getName() + ". Also, note that " |
| + "setting names are case-sensitive.)"); |
| } |
| } // for each cfg settings |
| |
| if (deployUrl == null) { |
| throw new DocgenException( |
| "The \"" + SETTING_DEPLOY_URL + "\" setting wasn't specified"); |
| } |
| if (offline == null) { |
| throw new DocgenException( |
| "The \"" + SETTING_OFFLINE |
| + "\" setting wasn't specified; it must be set to true or false"); |
| } |
| if (logo == null) { |
| throw new DocgenException( |
| "The \"" + SETTING_LOGO |
| + "\" setting wasn't specified; it must be set currently, as the layout reserves space for it."); |
| } |
| if (copyrightHolder == null) { |
| throw new DocgenException( |
| "The \"" + SETTING_COPYRIGHT_HOLDER + "\" setting wasn't specified."); |
| } |
| if (copyrightHolderSite == null) { |
| throw new DocgenException( |
| "The \"" + SETTING_COPYRIGHT_HOLDER_SITE + "\" setting wasn't specified."); |
| } |
| if (copyrightStartYear == null) { |
| throw new DocgenException( |
| "The \"" + SETTING_COPYRIGHT_START_YEAR + "\" setting wasn't specified."); |
| } |
| } |
| |
| // Ensure proper rank relations: |
| if (lowestPageTOCElemenRank.compareTo(lowestFileElemenRank) > 0) { |
| lowestPageTOCElemenRank = lowestFileElemenRank; |
| } |
| |
| // Ensure {@link #maxMainTOFDisplayDepth} is set: |
| if (maxMainTOFDisplayDepth == 0) { |
| maxMainTOFDisplayDepth = maxTOFDisplayDepth; |
| } |
| |
| templatesDir = new File(srcDir, DIR_TEMPLATES); |
| if (!templatesDir.exists()) { |
| templatesDir = null; |
| } |
| |
| if (contentDir == null) { |
| contentDir = srcDir; |
| } |
| |
| // Initialize state fields |
| |
| primaryIndexTermLookup = new HashMap<String, List<NodeModel>>(); |
| secondaryIndexTermLookup |
| = new HashMap<String, SortedMap<String, List<NodeModel>>>(); |
| elementsById = new HashMap<String, Element>(); |
| tocNodes = new ArrayList<TOCNode>(); |
| indexEntries = new ArrayList<String>(); |
| |
| // Setup FreeMarker: |
| |
| try { |
| Logger.selectLoggerLibrary(Logger.LIBRARY_NONE); |
| } catch (ClassNotFoundException e) { |
| throw new BugException(e); |
| } |
| |
| logger.info("Using FreeMarker " + Configuration.getVersion()); |
| fmConfig = new Configuration(Configuration.VERSION_2_3_25); |
| |
| TemplateLoader templateLoader = new ClassTemplateLoader( |
| Transform.class, "templates"); |
| if (templatesDir != null) { |
| templateLoader = new MultiTemplateLoader( |
| new TemplateLoader[] { new FileTemplateLoader(templatesDir), templateLoader }); |
| } |
| fmConfig.setTemplateLoader(templateLoader); |
| |
| fmConfig.setLocale(locale); |
| fmConfig.setTimeZone(timeZone); |
| |
| fmConfig.setDefaultEncoding(UTF_8.name()); |
| fmConfig.setOutputEncoding(UTF_8.name()); |
| |
| // Do the actual job: |
| |
| // - Load and validate the book XML |
| final File docFile; |
| { |
| final File docFile1 = new File(contentDir, FILE_BOOK); |
| if (docFile1.isFile()) { |
| docFile = docFile1; |
| } else { |
| final File docFile2 = new File(contentDir, FILE_ARTICLE); |
| if (docFile2.isFile()) { |
| docFile = docFile2; |
| } else { |
| throw new DocgenException("The book file is missing: " |
| + docFile1.getAbsolutePath() + " or " + docFile2.getAbsolutePath()); |
| } |
| } |
| } |
| Document doc = XMLUtil.loadDocBook5XML( |
| docFile, validate, validationOps, logger); |
| ignoredFilePathPatterns.add(FileUtil.globToRegexp(docFile.getName())); |
| |
| // - Post-edit and examine the DOM: |
| preprocessDOM(doc); |
| |
| // Resolve Docgen URL schemes in setting values: |
| // Olinks must come first: |
| if (olinks != null) { |
| for (Entry<String, String> olinkEnt : olinks.entrySet()) { |
| olinkEnt.setValue(resolveDocgenURL(SETTING_OLINKS, olinkEnt.getValue())); |
| } |
| } |
| if (tabs != null) { |
| for (Entry<String, String> tabEnt : tabs.entrySet()) { |
| tabEnt.setValue(resolveDocgenURL(SETTING_TABS, tabEnt.getValue())); |
| } |
| } |
| if (secondaryTabs != null) { |
| for (Map<String, String> tab : secondaryTabs.values()) { |
| tab.put("href", resolveDocgenURL(SETTING_SECONDARY_TABS, tab.get("href"))); |
| } |
| } |
| if (externalBookmarks != null) { |
| for (Entry<String, String> bookmarkEnt : externalBookmarks.entrySet()) { |
| bookmarkEnt.setValue(resolveDocgenURL(SETTING_EXTERNAL_BOOKMARKS, bookmarkEnt.getValue())); |
| } |
| } |
| if (socialLinks != null) { |
| for (Map<String, String> tab : socialLinks.values()) { |
| tab.put("href", resolveDocgenURL(SETTING_SOCIAL_LINKS, tab.get("href"))); |
| } |
| } |
| if (footerSiteMap != null) { |
| for (Map<String, String> links : footerSiteMap.values()) { |
| for (Map.Entry<String, String> link : links.entrySet()) { |
| link.setValue(resolveDocgenURL(SETTING_FOOTER_SITEMAP, link.getValue())); |
| } |
| } |
| } |
| if (logo != null) { |
| resolveLogoHref(logo); |
| } |
| if (sideTOCLogos != null) { |
| for (Logo logo : sideTOCLogos) { |
| resolveLogoHref(logo); |
| } |
| } |
| |
| // - Create destination directory: |
| if (!destDir.isDirectory() && !destDir.mkdirs()) { |
| throw new IOException("Failed to create destination directory: " |
| + destDir.getAbsolutePath()); |
| } |
| |
| // - Check internal book-marks: |
| for (Entry<String, String> ent : internalBookmarks.entrySet()) { |
| String id = ent.getValue(); |
| if (!elementsById.containsKey(id)) { |
| throw newCfgFileException(cfgFile, |
| SETTING_INTERNAL_BOOKMARKS, |
| "No element with id \"" + id |
| + "\" exists in the book."); |
| } |
| } |
| |
| // - Setup common data-model variables: |
| try { |
| // Settings: |
| fmConfig.setSharedVariable( |
| VAR_OFFLINE, offline); |
| fmConfig.setSharedVariable( |
| VAR_SIMPLE_NAVIGATION_MODE, simpleNavigationMode); |
| fmConfig.setSharedVariable( |
| VAR_DEPLOY_URL, deployUrl); |
| fmConfig.setSharedVariable( |
| VAR_ONLINE_TRACKER_HTML, onlineTrackerHTML); |
| fmConfig.setSharedVariable( |
| VAR_COOKIE_CONSENT_SCRIPT_URL, cookieConstentScriptURL); |
| fmConfig.setSharedVariable( |
| VAR_SHOW_EDITORAL_NOTES, showEditoralNotes); |
| fmConfig.setSharedVariable( |
| VAR_SHOW_XXE_LOGO, showXXELogo); |
| fmConfig.setSharedVariable( |
| VAR_SEARCH_KEY, searchKey); |
| fmConfig.setSharedVariable( |
| VAR_DISABLE_JAVASCRIPT, disableJavaScript); |
| fmConfig.setSharedVariable( |
| VAR_OLINKS, olinks); |
| fmConfig.setSharedVariable( |
| VAR_NUMBERED_SECTIONS, numberedSections); |
| fmConfig.setSharedVariable( |
| VAR_LOGO, logo); |
| fmConfig.setSharedVariable( |
| VAR_SIDE_TOC_LOGOS, sideTOCLogos); |
| fmConfig.setSharedVariable( |
| VAR_COPYRIGHT_HOLDER, copyrightHolder); |
| fmConfig.setSharedVariable( |
| VAR_COPYRIGHT_HOLDER_SITE, copyrightHolderSite); |
| fmConfig.setSharedVariable( |
| VAR_COPYRIGHT_SUFFIX, copyrightSuffix); |
| fmConfig.setSharedVariable( |
| VAR_COPYRIGHT_START_YEAR, copyrightStartYear); |
| fmConfig.setSharedVariable( |
| VAR_COPYRIGHT_COMMENT, copyrightComment); |
| fmConfig.setSharedVariable( |
| VAR_COPYRIGHT_JAVA_COMMENT, copyrightJavaComment); |
| fmConfig.setSharedVariable( |
| VAR_TABS, tabs); |
| fmConfig.setSharedVariable( |
| VAR_SECONDARY_TABS, secondaryTabs); |
| fmConfig.setSharedVariable( |
| VAR_SOCIAL_LINKS, socialLinks); |
| fmConfig.setSharedVariable( |
| VAR_FOOTER_SITEMAP, footerSiteMap); |
| fmConfig.setSharedVariable( |
| VAR_EXTERNAL_BOOKMARDS, externalBookmarks); |
| fmConfig.setSharedVariable( |
| VAR_INTERNAL_BOOKMARDS, internalBookmarks); |
| fmConfig.setSharedVariable( |
| VAR_ROOT_ELEMENT, doc.getDocumentElement()); |
| |
| // Calculated data: |
| { |
| Date generationTime; |
| String generationTimeStr = System.getProperty(SYSPROP_GENERATION_TIME); |
| if (generationTimeStr == null) { |
| generationTime = new Date(); |
| } else { |
| try { |
| generationTime = DateUtil.parseISO8601DateTime(generationTimeStr, DateUtil.UTC, |
| new DateUtil.TrivialCalendarFieldsToDateConverter()); |
| } catch (DateParseException e) { |
| throw new DocgenException( |
| "Malformed \"" + SYSPROP_GENERATION_TIME |
| + "\" system property value: " + generationTimeStr, e); |
| } |
| } |
| fmConfig.setSharedVariable(VAR_TRANSFORM_START_TIME, generationTime); |
| } |
| fmConfig.setSharedVariable( |
| VAR_INDEX_ENTRIES, indexEntries); |
| int tofCntLv1 = countTOFEntries(tocNodes.get(0), 1); |
| int tofCntLv2 = countTOFEntries(tocNodes.get(0), 2); |
| fmConfig.setSharedVariable( |
| VAR_SHOW_NAVIGATION_BAR, |
| tofCntLv1 != 0 |
| || internalBookmarks.size() != 0 |
| || externalBookmarks.size() != 0); |
| fmConfig.setSharedVariable( |
| VAR_SHOW_BREADCRUMB, tofCntLv1 != tofCntLv2); |
| |
| // Helper methods and directives: |
| fmConfig.setSharedVariable( |
| "NodeFromID", nodeFromID); |
| fmConfig.setSharedVariable( |
| "CreateLinkFromID", createLinkFromID); |
| fmConfig.setSharedVariable( |
| "primaryIndexTermLookup", primaryIndexTermLookup); |
| fmConfig.setSharedVariable( |
| "secondaryIndexTermLookup", secondaryIndexTermLookup); |
| fmConfig.setSharedVariable( |
| "CreateLinkFromNode", createLinkFromNode); |
| } catch (TemplateModelException e) { |
| throw new BugException(e); |
| } |
| |
| // - Generate ToC JSON-s: |
| { |
| logger.info("Generating ToC JSON..."); |
| Template template = fmConfig.getTemplate(FILE_TOC_JSON_TEMPLATE); |
| try (Writer wr = new BufferedWriter( |
| new OutputStreamWriter( |
| new FileOutputStream( |
| new File(destDir, FILE_TOC_JSON_OUTPUT)), |
| UTF_8))) { |
| try { |
| SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); |
| dataModel.put(VAR_JSON_TOC_ROOT, tocNodes.get(0)); |
| template.process(dataModel, wr, null, NodeModel.wrap(doc)); |
| } catch (TemplateException e) { |
| throw new BugException("Failed to generate ToC JSON " |
| + "(see cause exception).", e); |
| } |
| } |
| } |
| |
| // - Generate Sitemap XML: |
| { |
| logger.info("Generating Sitemap XML..."); |
| Template template = fmConfig.getTemplate(FILE_SITEMAP_XML_TEMPLATE); |
| try (Writer wr = new BufferedWriter( |
| new OutputStreamWriter( |
| new FileOutputStream( |
| new File(destDir, FILE_SITEMAP_XML_OUTPUT)), |
| UTF_8))) { |
| try { |
| SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); |
| dataModel.put(VAR_JSON_TOC_ROOT, tocNodes.get(0)); |
| template.process(dataModel, wr, null, NodeModel.wrap(doc)); |
| } catch (TemplateException e) { |
| throw new BugException("Failed to generate Sitemap XML" |
| + "(see cause exception).", e); |
| } |
| } |
| } |
| |
| |
| // - Generate the HTML-s: |
| logger.info("Generating HTML files..."); |
| int htmlFileCounter = 0; |
| for (TOCNode tocNode : tocNodes) { |
| if (tocNode.getOutputFileName() != null) { |
| try { |
| currentFileTOCNode = tocNode; |
| try { |
| // All output-file-specific processing comes here. |
| htmlFileCounter += generateHTMLFile(); |
| } finally { |
| currentFileTOCNode = null; |
| } |
| } catch (freemarker.core.StopException e) { |
| throw new DocgenException(e.getMessage()); |
| } catch (TemplateException e) { |
| throw new BugException(e); |
| } |
| } |
| } |
| |
| if (!offline && searchKey != null) { |
| try { |
| generateSearchResultsHTMLFile(doc); |
| htmlFileCounter++; |
| } catch (freemarker.core.StopException e) { |
| throw new DocgenException(e.getMessage()); |
| } catch (TemplateException e) { |
| throw new BugException(e); |
| } |
| } |
| |
| // - Copy the standard statics: |
| logger.info("Copying common static files..."); |
| copyCommonStatic("docgen.min.css"); |
| copyCommonStatic("img/patterned-bg.png"); |
| |
| copyCommonStatic("fonts/icomoon.eot"); |
| copyCommonStatic("fonts/icomoon.svg"); |
| copyCommonStatic("fonts/icomoon.ttf"); |
| copyCommonStatic("fonts/icomoon.woff"); |
| copyCommonStatic("fonts/NOTICE"); |
| |
| if (showXXELogo) { |
| copyCommonStatic("img/xxe.png"); |
| } |
| if (!disableJavaScript) { |
| copyCommonStatic("main.min.js"); |
| } |
| |
| // - Copy the custom statics: |
| logger.info("Copying custom static files..."); |
| int bookSpecStaticFileCounter = FileUtil.copyDir(contentDir, destDir, ignoredFilePathPatterns); |
| |
| // - Eclipse ToC: |
| if (generateEclipseTOC) { |
| if (simpleNavigationMode) { |
| throw new DocgenException("Eclipse ToC generation is untested/unsupported with simpleNavigationMode=true."); |
| } |
| |
| logger.info("Generating Eclipse ToC..."); |
| Template template = fmConfig.getTemplate(FILE_ECLIPSE_TOC_TEMPLATE); |
| try (Writer wr = new BufferedWriter( |
| new OutputStreamWriter( |
| new FileOutputStream( |
| new File(destDir, FILE_ECLIPSE_TOC_OUTPUT)), |
| UTF_8))) { |
| try { |
| SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); |
| if (eclipseLinkTo != null) { |
| dataModel.put(VAR_ECLIPSE_LINK_TO, eclipseLinkTo); |
| } |
| template.process(dataModel, wr, null, NodeModel.wrap(doc)); |
| } catch (TemplateException e) { |
| throw new BugException("Failed to generate Eclipse ToC " |
| + "(see cause exception).", e); |
| } |
| } |
| } |
| |
| // - Report summary: |
| logger.info( |
| "Done: " |
| + htmlFileCounter + " HTML-s + " |
| + bookSpecStaticFileCounter + " custom statics + commons" |
| + (generateEclipseTOC ? " + Eclipse ToC" : "")); |
| } |
| |
| private void resolveLogoHref(Logo logo) throws DocgenException { |
| String logoHref = logo.getHref(); |
| if (logoHref != null) { |
| logo.setHref(resolveDocgenURL(SETTING_LOGO, logoHref)); |
| } |
| } |
| |
| /** |
| * Resolves the URL if it uses the {@code "olink:"} or {@code "id:"} schema, returns it as if otherwise. |
| */ |
| private String resolveDocgenURL(String settingName, String url) throws DocgenException { |
| if (url.startsWith(OLINK_SCHEMA_START)) { |
| String oLinkName = url.substring(OLINK_SCHEMA_START.length()); |
| String resolvedOLink = olinks.get(oLinkName); |
| if (resolvedOLink == null) { |
| throw new DocgenException("Undefined olink used inside configuration setting " |
| + StringUtil.jQuote(settingName) |
| + ": " + StringUtil.jQuote(oLinkName)); |
| } |
| return resolveDocgenURL(settingName, resolvedOLink); |
| } else if (url.startsWith(ID_SCHEMA_START)) { |
| String id = url.substring(ID_SCHEMA_START.length()); |
| try { |
| return createLinkFromId(id); |
| } catch (DocgenException e) { |
| throw new DocgenException("Can't resolve id inside configuration setting " |
| + StringUtil.jQuote(settingName) |
| + ": " + StringUtil.jQuote(id), |
| e); |
| } |
| } else { |
| return url; |
| } |
| } |
| |
| private DocgenException newCfgFileException( |
| File cfgFile, String settingName, String desc) { |
| settingName = settingName.replace(".", "\" per \""); |
| return newCfgFileException(cfgFile, "Wrong value for setting \"" |
| + settingName + "\": " + desc); |
| } |
| |
| private DocgenException newCfgFileException(File cfgFile, String desc) { |
| return newCfgFileException(cfgFile, desc, (Throwable) null); |
| } |
| |
| private DocgenException newCfgFileException(File cfgFile, String desc, |
| Throwable cause) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("Wrong configuration"); |
| if (cfgFile != null) { |
| sb.append(" file \""); |
| sb.append(cfgFile.getAbsolutePath()); |
| sb.append("\""); |
| } |
| sb.append(": "); |
| sb.append(desc); |
| return new DocgenException(sb.toString(), cause); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Map<String, Object> castSettingToMapWithStringKeys( |
| File cfgFile, String settingName, Object settingValue) |
| throws DocgenException { |
| if (!(settingValue instanceof Map)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be a map (like { key1: value1, key2: value2 }), but " |
| + "it's a " + CJSONInterpreter.cjsonTypeOf(settingValue) |
| + "."); |
| } |
| for (Object key : ((Map<?, ?>) settingValue).keySet()) { |
| if (!(key instanceof String)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "All keys should be String-s, but one of them is a(n) " |
| + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); |
| } |
| } |
| return (Map<String, Object>) settingValue; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private List<String> castSettingToStringList( |
| File cfgFile, String settingName, Object settingValue) |
| throws DocgenException { |
| List<?> settingValueAsList = castSettingToList(cfgFile, settingName, settingValue); |
| for (int i = 0; i < settingValueAsList.size(); i++) { |
| Object listItem = settingValueAsList.get(i); |
| if (!(listItem instanceof String)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be a list of String-s (like [\"value1\", \"value2\", ... \"valueN\"]), but at index " |
| + i +" (0-based) there's a " + CJSONInterpreter.cjsonTypeOf(listItem) |
| + "."); |
| } |
| } |
| return (List<String>) settingValue; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private List<Map<String, Object>> castSettingToListOfMapsWithStringKeys( |
| File cfgFile, String settingName, Object settingValue) |
| throws DocgenException { |
| List<?> settingValueAsList = castSettingToList(cfgFile, settingName, settingValue); |
| for (int i = 0; i < settingValueAsList.size(); i++) { |
| castSettingToMapWithStringKeys(cfgFile, settingName + "[" + i + "]", settingValueAsList.get(i)); |
| } |
| return (List) settingValue; |
| } |
| |
| private List<?> castSettingToList(File cfgFile, String settingName, Object settingValue) throws DocgenException { |
| if (!(settingValue instanceof List)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be a list (like [value1, value2, ... valueN]), but " |
| + "it's a " + CJSONInterpreter.cjsonTypeOf(settingValue) |
| + "."); |
| } |
| return (List<?>) settingValue; |
| } |
| |
| private String castSettingToString(File cfgFile, |
| String settingName, Object settingValue) throws DocgenException { |
| if (!(settingValue instanceof String)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be a string, but it's a " |
| + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); |
| } |
| return (String) settingValue; |
| } |
| |
| private boolean caseSettingToBoolean(File cfgFile, |
| String settingName, Object settingValue) throws DocgenException { |
| if (!(settingValue instanceof Boolean)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be a boolean (i.e., true or false), but it's a " |
| + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); |
| } |
| return (Boolean) settingValue; |
| } |
| |
| private int castSettingToInt(File cfgFile, |
| String settingName, Object settingValue) |
| throws DocgenException { |
| |
| if (!(settingValue instanceof Number)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be an number, but it's a " |
| + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); |
| } |
| if (!(settingValue instanceof Integer)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be an integer number (32 bits max), but it's: " |
| + settingValue); |
| } |
| return ((Integer) settingValue).intValue(); |
| } |
| |
| /* Unused at the moment |
| @SuppressWarnings("unchecked") |
| private List<String> castSettingToListOfStrings(File cfgFile, |
| String settingName, Object settingValue) throws DocgenException { |
| if (!(settingValue instanceof List)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be a list, but it's a " |
| + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); |
| } |
| List ls = (List) settingValue; |
| |
| for (Object i : ls) { |
| if (!(i instanceof String)) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "Should be a list of strings, but one if the list items " |
| + "is a " + CJSONInterpreter.cjsonTypeOf(i) + "."); |
| } |
| } |
| |
| return ls; |
| } |
| */ |
| |
| private String castSettingValueMapValueToString(File cfgFile, |
| String settingName, Object mapEntryValue) throws DocgenException { |
| if (!(mapEntryValue instanceof String)) { |
| throw newCfgFileException(cfgFile, settingName, |
| "The values in the key-value pairs of this map must be " |
| + "strings, but some of them is a " |
| + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + "."); |
| } |
| return (String) mapEntryValue; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private Map<String, String> castSettingValueMapValueToMapOfStringString(File cfgFile, |
| String settingName, Object mapEntryValue, Set<String> requiredKeys, Set<String> optionalKeys) |
| throws DocgenException { |
| if (!(mapEntryValue instanceof Map)) { |
| throw newCfgFileException(cfgFile, settingName, |
| "The values in the key-value pairs of this map must be " |
| + "Map-s, but some of them is a " |
| + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + "."); |
| } |
| |
| if (requiredKeys == null) requiredKeys = Collections.emptySet(); |
| if (optionalKeys == null) optionalKeys = Collections.emptySet(); |
| |
| Map<?, ?> mapEntryValueAsMap = (Map<?, ?>) mapEntryValue; |
| for (Entry<?, ?> valueEnt : mapEntryValueAsMap.entrySet()) { |
| Object key = valueEnt.getKey(); |
| if (!(key instanceof String)) { |
| throw newCfgFileException(cfgFile, settingName, |
| "The values in the key-value pairs of this map must be " |
| + "Map<String, String>-s, but some of the keys is a " |
| + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + "."); |
| } |
| if (!(valueEnt.getValue() instanceof String)) { |
| throw newCfgFileException(cfgFile, settingName, |
| "The values in the key-value pairs of this map must be " |
| + "Map<String, String>-s, but some of the values is a " |
| + CJSONInterpreter.cjsonTypeOf(valueEnt.getValue()) + "."); |
| } |
| if (!requiredKeys.contains(key) && !optionalKeys.contains(key)) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("Unsupported key: "); |
| sb.append(StringUtil.jQuote(key)); |
| sb.append(". Supported keys are: "); |
| boolean isFirst = true; |
| for (String supportedKey : requiredKeys) { |
| if (!isFirst) { |
| sb.append(", "); |
| } else { |
| isFirst = false; |
| } |
| sb.append(StringUtil.jQuote(supportedKey)); |
| } |
| for (String supportedKey : optionalKeys) { |
| if (!isFirst) { |
| sb.append(", "); |
| } else { |
| isFirst = false; |
| } |
| sb.append(StringUtil.jQuote(supportedKey)); |
| } |
| throw newCfgFileException(cfgFile, settingName, sb.toString()); |
| } |
| } |
| for (String requiredKey : requiredKeys) { |
| if (!mapEntryValueAsMap.containsKey(requiredKey)) { |
| throw newCfgFileException(cfgFile, settingName, |
| "Missing map key from nested Map: " + requiredKey); |
| } |
| } |
| return (Map<String, String>) mapEntryValue; |
| } |
| |
| private Logo castMapToLogo(File cfgFile, final String settingName, Map<String, Object> map) |
| throws DocgenException { |
| Logo logo = new Logo(); |
| for (Entry<String, Object> ent : map.entrySet()) { |
| String key = ent.getKey(); |
| String value = castSettingValueMapValueToString(cfgFile, settingName, ent.getValue()); |
| switch (key) { |
| case SETTING_LOGO_KEY_SRC: |
| logo.setSrc(value); |
| break; |
| case SETTING_LOGO_KEY_ALT: |
| logo.setAlt(value); |
| break; |
| case SETTING_LOGO_KEY_HREF: |
| logo.setHref(value); |
| break; |
| default: |
| throw newCfgFileException(cfgFile, SETTING_LOGO, "Unknown logo option: " + StringUtil.jQuote(key)); |
| } |
| } |
| |
| if (logo.getSrc() == null) { |
| throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_SRC); |
| } |
| if (logo.getAlt() == null) { |
| throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_ALT); |
| } |
| if (logo.getHref() == null) { |
| throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_HREF); |
| } |
| |
| return logo; |
| } |
| |
| private String getFileContentForSetting(File cfgFile, |
| String settingName, Object settingValue) throws DocgenException { |
| String settingValueStr = castSettingToString(cfgFile, settingName, settingValue); |
| File f = new File(getSourceDirectory(), settingValueStr); |
| if (!f.exists()) { |
| throw newCfgFileException( |
| cfgFile, settingName, |
| "File not found: " + f.toPath()); |
| } |
| try { |
| return FileUtil.loadString(f, UTF_8); |
| } catch (IOException e) { |
| throw newCfgFileException( |
| cfgFile, "Error while reading file for setting \"" + settingName + "\": " + f.toPath(), |
| e); |
| } |
| } |
| |
| private void copyCommonStatic(String staticFileName) throws IOException, DocgenException { |
| String resourcePath = "statics/" + staticFileName; |
| try (InputStream in = Transform.class.getResourceAsStream(resourcePath)) { |
| if (in == null) { |
| throw new IOException("Failed to open class-loader resource: " + resourcePath + " relatively to " |
| + Transform.class.getPackage().getName()); |
| } |
| |
| if (staticFileName.endsWith(".css") || staticFileName.endsWith(".js")) { |
| // ISO-8859-1 will be good enough as far as the resource isn't UTF-16 or EBCDIC: |
| final Charset fileCharset = StandardCharsets.ISO_8859_1; |
| String content = FileUtil.loadString(in, fileCharset); |
| final String eol = TextUtil.detectEOL(content, "\n"); |
| |
| // If we have an initial comment, then that must be a copyright header, which we will remove. |
| if (content.startsWith("/*")) { |
| int commentEnd = content.indexOf("*/"); |
| if (commentEnd == -1) { |
| throw new BugException("Unclosed initial \"/*\" in resource " + resourcePath); |
| } |
| commentEnd += 2; |
| String comment = content.substring(0, commentEnd); |
| if (!comment.contains("Copyright") && !comment.contains("copyright") |
| && !comment.contains("License") && !comment.contains("license")) { |
| throw new BugException("The initial /*...*/ comments doesn't look like a copyright header " |
| + "in resource " + resourcePath); |
| } |
| |
| // Include an EOL after the comment, if there's any. |
| for (int i = 0; i < 2; i++) { |
| if (commentEnd < content.length()) { |
| char c = content.charAt(commentEnd); |
| if (c == '\n') { |
| commentEnd++; |
| } else if (c == '\r') { |
| commentEnd++; |
| if (commentEnd < content.length() && content.charAt(commentEnd) == '\n') { |
| commentEnd++; |
| } |
| } |
| } |
| } |
| |
| // Remove existing copyright header: |
| content = content.substring(commentEnd); |
| } |
| |
| if (offline && copyrightComment != null) { |
| // Add copyright comment: |
| StringBuilder sb = new StringBuilder(TextUtil.normalizeEOL(copyrightJavaComment, eol)); |
| sb.append(eol); |
| if (content.length() > 0 && content.charAt(0) != '\n' && content.charAt(0) != '\r') { |
| sb.append(eol); |
| } |
| sb.append(content); |
| content = sb.toString(); |
| } |
| |
| Path destSubdir = destDir.toPath().resolve("docgen-resources"); |
| Files.createDirectories(destSubdir); |
| Files.write(destSubdir.resolve(staticFileName), content.getBytes(fileCharset)); |
| } else { |
| FileUtil.copyResourceIntoFile( |
| Transform.class, "statics", staticFileName, |
| new File(destDir, "docgen-resources")); |
| } |
| } |
| } |
| |
| /** |
| * Adds attribute <tt>id</tt> to elements that are in |
| * <code>idAttrElements</code>, but has no id attribute yet. |
| * Adding id-s is useful to create more precise HTML cross-links later. |
| */ |
| private void preprocessDOM(Document doc) |
| throws SAXException, DocgenException { |
| NodeModel.simplify(doc); |
| preprocessDOM_applyRemoveNodesWhenOnlineSetting(doc); |
| preprocessDOM_addRanks(doc); |
| preprocessDOM_misc(doc); |
| preprocessDOM_buildTOC(doc); |
| } |
| |
| private static final class PreprocessDOMMisc_GlobalState { |
| private int lastId; |
| |
| /** Style silencer: notAUtiltiyClass() never used */ |
| private PreprocessDOMMisc_GlobalState() { |
| notAUtiltiyClass(); |
| } |
| |
| /** CheckStyle silencer */ |
| void notAUtiltiyClass() { |
| // Nop |
| } |
| } |
| |
| private static final class PreprocessDOMMisc_ParentSectState { |
| private int upperRomanNumber = 1; |
| private int lowerRomanNumber = 1; |
| private int arabicNumber = 1; |
| private int upperLatinNumber = 1; |
| private int unitedNumber = 1; |
| |
| /** Style silencer: notAUtiltiyClass() never used */ |
| private PreprocessDOMMisc_ParentSectState() { |
| notAUtiltiyClass(); |
| } |
| |
| /** CheckStyle silencer */ |
| void notAUtiltiyClass() { |
| // Nop |
| } |
| } |
| |
| private void preprocessDOM_misc(Document doc) |
| throws SAXException, DocgenException { |
| preprocessDOM_misc_inner(doc, |
| new PreprocessDOMMisc_GlobalState(), |
| new PreprocessDOMMisc_ParentSectState()); |
| indexEntries = new ArrayList<String>(primaryIndexTermLookup.keySet()); |
| Collections.sort(indexEntries, Collator.getInstance(locale)); |
| } |
| |
| private void preprocessDOM_misc_inner( |
| Node node, |
| PreprocessDOMMisc_GlobalState globalState, |
| PreprocessDOMMisc_ParentSectState parentSectState) |
| throws SAXException, DocgenException { |
| if (node instanceof Element) { |
| Element elem = (Element) node; |
| |
| // xml:id -> id: |
| String id = XMLUtil.getAttribute(elem, "xml:id"); |
| if (id != null) { |
| if (id.startsWith(AUTO_ID_PREFIX)) { |
| throw new DocgenException( |
| XMLUtil.theSomethingElement(elem, true) |
| + " uses a reserved xml:id, " |
| + TextUtil.jQuote(id) + ". All ID-s starting with " |
| + "\"" + AUTO_ID_PREFIX + "\" are reserved for " |
| + "Docgen."); |
| } |
| if (id.startsWith(DOCGEN_ID_PREFIX)) { |
| throw new DocgenException( |
| XMLUtil.theSomethingElement(elem, true) |
| + " uses a reserved xml:id, " |
| + TextUtil.jQuote(id) + ". All ID-s starting with " |
| + "\"" + DOCGEN_ID_PREFIX + "\" are reserved for " |
| + "Docgen."); |
| } |
| elem.setAttribute("id", id); |
| } |
| |
| final String elemName = node.getNodeName(); |
| |
| // Add auto id-s: |
| if (id == null && GUARANTEED_ID_ELEMENTS.contains(elemName)) { |
| globalState.lastId++; |
| id = AUTO_ID_PREFIX + globalState.lastId; |
| elem.setAttribute("id", id); |
| } |
| if (id != null) { |
| elementsById.put(id, elem); |
| } |
| |
| // Add default titles: |
| if (elemName.equals(E_PREFACE) |
| || elemName.equals(E_GLOSSARY) |
| || elemName.equals(E_INDEX)) { |
| ensureTitleExists( |
| elem, |
| Character.toUpperCase(elemName.charAt(0)) |
| + elemName.substring(1)); |
| |
| // Simplify tables: |
| } else if ( |
| (elemName.equals(E_INFORMALTABLE) |
| || elemName.equals(E_TABLE)) |
| && elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { |
| TableSimplifier.simplify(elem); |
| // Collect index terms: |
| } else if (elemName.equals(E_INDEXTERM)) { |
| addIndexTerm(node); |
| } else if (elemName.equals(E_IMAGEDATA)) { |
| String ref = XMLUtil.getAttribute(elem, A_FILEREF); |
| String loRef = ref.toLowerCase(); |
| if (!loRef.startsWith("http://") |
| && !loRef.startsWith("https://") |
| && !ref.startsWith("/")) { |
| if (!new File(contentDir, ref.replace('/', File.separatorChar)).isFile()) { |
| throw new DocgenException( |
| XMLUtil.theSomethingElement(elem) + " refers " |
| + "to a missing file: \"" |
| + ref.replace("\"", """) + "\""); |
| } |
| } |
| if (loRef.endsWith(".svg")) { |
| String pngRef = ref.substring(0, ref.length() - 4) + ".png"; |
| if (!new File(contentDir, pngRef.replace('/', File.separatorChar)).isFile()) { |
| throw new DocgenException( |
| XMLUtil.theSomethingElement(elem) |
| + " refers to an SVG file for which the fallback PNG file is missing: \"" |
| + pngRef.replace("\"", """) + "\""); |
| } |
| } |
| } |
| |
| // Adding title prefixes to document structure elements: |
| if (DOCUMENT_STRUCTURE_ELEMENTS.contains(elemName)) { |
| final String prefix; |
| if (elem.getParentNode() instanceof Document) { |
| // The document element is never prefixed |
| prefix = null; |
| } else if (hasPrefaceLikeParent(elem)) { |
| prefix = null; |
| } else if (numberedSections |
| && elemName.equals(E_SECTION)) { |
| prefix = String.valueOf( |
| parentSectState.arabicNumber++); |
| } else if (elemName.equals(E_CHAPTER)) { |
| prefix = String.valueOf( |
| parentSectState.arabicNumber++); |
| } else if (elemName.equals(E_PART)) { |
| prefix = TextUtil.toUpperRomanNumber( |
| parentSectState.upperRomanNumber++); |
| } else if (elemName.equals(E_APPENDIX)) { |
| prefix = TextUtil.toUpperLatinNumber( |
| parentSectState.upperLatinNumber++); |
| } else if (elemName.equals(E_ARTICLE)) { |
| prefix = TextUtil.toLowerRomanNumber( |
| parentSectState.lowerRomanNumber++); |
| } else { |
| prefix = null; |
| } |
| |
| if (prefix != null) { |
| final String fullPrefix; |
| final Node parent = elem.getParentNode(); |
| if (parent instanceof Element |
| // Don't inherit prefix from "part" rank: |
| && !parent.getLocalName().equals(E_PART) |
| // Don't inherit prefix from "article": |
| && !parent.getLocalName().equals(E_ARTICLE)) { |
| String inhPrefix = XMLUtil.getAttribute( |
| (Element) parent, A_DOCGEN_TITLE_PREFIX); |
| if (inhPrefix != null) { |
| if (inhPrefix.endsWith(".")) { |
| fullPrefix = inhPrefix + prefix; |
| } else { |
| fullPrefix = inhPrefix + "." + prefix; |
| } |
| } else { |
| fullPrefix = prefix; |
| } |
| } else { |
| fullPrefix = prefix; |
| } |
| |
| elem.setAttribute(A_DOCGEN_TITLE_PREFIX, fullPrefix); |
| } // if prefix != null |
| |
| elem.setAttribute( |
| A_DOCGEN_UNITED_NUMBERING, |
| String.valueOf(parentSectState.unitedNumber++)); |
| |
| // We will be the parent document structure element of the soon |
| // processed children: |
| parentSectState = new PreprocessDOMMisc_ParentSectState(); |
| } // if document structure element |
| } // if element |
| |
| NodeList children = node.getChildNodes(); |
| int ln = children.getLength(); |
| for (int i = 0; i < ln; i++) { |
| preprocessDOM_misc_inner( |
| children.item(i), |
| globalState, parentSectState); |
| } |
| } |
| |
| private void preprocessDOM_applyRemoveNodesWhenOnlineSetting(Document doc) throws DocgenException { |
| if (offline || removeNodesWhenOnline == null || removeNodesWhenOnline.isEmpty()) return; |
| |
| HashSet<String> idsToRemoveLeft = new HashSet<String>(removeNodesWhenOnline); |
| preprocessDOM_applyRemoveNodesWhenOnlineSetting_inner( |
| doc.getDocumentElement(), idsToRemoveLeft); |
| if (!idsToRemoveLeft.isEmpty()) { |
| throw new DocgenException( |
| "These xml:id-s, specified in the \"" + SETTING_REMOVE_NODES_WHEN_ONLINE |
| + "\" configuration setting, wasn't found in the document: " + idsToRemoveLeft); |
| } |
| } |
| |
| private void preprocessDOM_applyRemoveNodesWhenOnlineSetting_inner(Element elem, Set<String> idsToRemoveLeft) { |
| Node child = elem.getFirstChild(); |
| while (child != null && !idsToRemoveLeft.isEmpty()) { |
| Element childElemToBeRemoved = null; |
| if (child instanceof Element) { |
| Element childElem = (Element) child; |
| String id = XMLUtil.getAttribute(childElem, "xml:id"); |
| if (id != null && idsToRemoveLeft.remove(id)) { |
| childElemToBeRemoved = childElem; |
| } |
| if (!idsToRemoveLeft.isEmpty()) { |
| preprocessDOM_applyRemoveNodesWhenOnlineSetting_inner(childElem, idsToRemoveLeft); |
| } |
| } |
| child = child.getNextSibling(); |
| if (childElemToBeRemoved != null) { |
| elem.removeChild(childElemToBeRemoved); |
| } |
| } |
| } |
| |
| /** |
| * Annotates the document structure nodes with so called ranks. |
| * About ranks see: {@link #setting_lowestFileElementRank}. |
| */ |
| private void preprocessDOM_addRanks(Document doc) |
| throws DocgenException { |
| Element root = doc.getDocumentElement(); |
| String rootName = root.getLocalName(); |
| if (rootName.equals(E_BOOK)) { |
| root.setAttribute( |
| A_DOCGEN_RANK, DocumentStructureRank.BOOK.toString()); |
| preprocessDOM_addRanks_underBookRank(root); |
| } else if (rootName.equals(E_ARTICLE)) { |
| root.setAttribute( |
| A_DOCGEN_RANK, DocumentStructureRank.CHAPTER.toString()); |
| preprocessDOM_addRanks_underChapterRankOrDeeper(root, 0); |
| } else { |
| throw new DocgenException("The \"" + rootName + "\" element is " |
| + "unsupported as root element."); |
| } |
| } |
| |
| private void preprocessDOM_addRanks_underBookRank( |
| Element root) throws DocgenException { |
| |
| // Find the common rank: |
| DocumentStructureRank commonRank = null; |
| for (Element child : XMLUtil.childrenElementsOf(root)) { |
| String name = child.getLocalName(); |
| if (name.equals(E_PART)) { |
| if (commonRank != null |
| && !commonRank.equals(DocumentStructureRank.PART)) { |
| throw new DocgenException("Bad document structure: " |
| + XMLUtil.theSomethingElement(child) + " is on the " |
| + "same ToC level with a \"" + E_CHAPTER |
| + "\" element."); |
| } |
| commonRank = DocumentStructureRank.PART; |
| } else if (name.equals(E_CHAPTER)) { |
| if (commonRank != null |
| && !commonRank.equals(DocumentStructureRank.CHAPTER)) { |
| throw new DocgenException("Bad document structure: " |
| + XMLUtil.theSomethingElement(child) + " is on the " |
| + "same ToC level with a \"" + E_PART |
| + "\" element."); |
| } |
| commonRank = DocumentStructureRank.CHAPTER; |
| } |
| } |
| if (commonRank == null) { |
| commonRank = DocumentStructureRank.CHAPTER; |
| } |
| |
| // Apply the common rank plus go deeper: |
| for (Element child : XMLUtil.childrenElementsOf(root)) { |
| if (DOCUMENT_STRUCTURE_ELEMENTS.contains(child.getLocalName())) { |
| child.setAttribute( |
| A_DOCGEN_RANK, commonRank.toString()); |
| // Even if this node received part rank, its children will not |
| // "feel like" being the children of a true part, unless its |
| // indeed a part: |
| if (child.getLocalName().equals(E_PART)) { |
| preprocessDOM_addRanks_underTruePart(child); |
| } else { |
| preprocessDOM_addRanks_underChapterRankOrDeeper( |
| child, 0); |
| } |
| } |
| } |
| } |
| |
| private void preprocessDOM_addRanks_underTruePart( |
| Node parent) throws DocgenException { |
| for (Element child : XMLUtil.childrenElementsOf(parent)) { |
| if (DOCUMENT_STRUCTURE_ELEMENTS.contains(child.getLocalName())) { |
| child.setAttribute( |
| A_DOCGEN_RANK, |
| DocumentStructureRank.CHAPTER.toString()); |
| preprocessDOM_addRanks_underChapterRankOrDeeper(child, 0); |
| } |
| } |
| } |
| |
| private void preprocessDOM_addRanks_underChapterRankOrDeeper( |
| Element parent, int underSectionRank) throws DocgenException { |
| for (Element child : XMLUtil.childrenElementsOf(parent)) { |
| if (DOCUMENT_STRUCTURE_ELEMENTS.contains(child.getLocalName())) { |
| if (child.getLocalName().equals(E_SIMPLESECT)) { |
| child.setAttribute( |
| A_DOCGEN_RANK, |
| DocumentStructureRank.SIMPLESECT.toString()); |
| // Note: simplesection-s are leafs in the ToC hierarchy. |
| } else { |
| if (underSectionRank + 1 > DocgenRestrictionsValidator |
| .MAX_SECTION_NESTING_LEVEL) { |
| throw new DocgenException("Too deep ToC nesting for " |
| + XMLUtil.theSomethingElement(child) |
| + ": rank bellow " |
| + DocumentStructureRank.sectionToString( |
| DocgenRestrictionsValidator |
| .MAX_SECTION_NESTING_LEVEL)); |
| } |
| |
| child.setAttribute( |
| A_DOCGEN_RANK, |
| DocumentStructureRank.sectionToString( |
| underSectionRank + 1)); |
| |
| preprocessDOM_addRanks_underChapterRankOrDeeper( |
| child, underSectionRank + 1); |
| } |
| } |
| } |
| } |
| |
| private void preprocessDOM_buildTOC(Document doc) throws DocgenException { |
| preprocessDOM_buildTOC_inner(doc, 0, null); |
| if (tocNodes.size() > 0) { |
| preprocessDOM_buildTOC_checkEnsureHasIndexHhml(tocNodes); |
| |
| preprocessDOM_buildTOC_checkTOCTopology(tocNodes.get(0)); |
| |
| if (!tocNodes.get(0).isFileElement()) { |
| throw new BugException( |
| "The root ToC node must be a file-element."); |
| } |
| preprocessDOM_buildTOC_checkFileTopology(tocNodes.get(0)); |
| |
| if (simpleNavigationMode) { |
| // Must do it at the end: We need the docgen_... XML attributes here, and we must be past the |
| // TOC topology checks. |
| for (TOCNode tocNode : tocNodes) { |
| // Don't generate a file for pages that would only contain a table of contents |
| if (tocNode.isFileElement() |
| && (tocNode.getParent() == null || !hasContentInTheSameFile(tocNode))) { |
| tocNode.setOutputFileName(null); |
| tocNode.getElement().setAttribute(A_DOCGEN_NOT_ADDRESSABLE, "true"); |
| } |
| } |
| } |
| |
| if (!validationOps.getOutputFilesCanUseAutoID()) { |
| for (TOCNode tocNode : tocNodes) { |
| String outputFileName = tocNode.getOutputFileName(); |
| if (outputFileName != null && outputFileName.startsWith(AUTO_ID_PREFIX)) { |
| throw new DocgenException(XMLUtil.theSomethingElement(tocNode.getElement(), true) |
| + " has automatically generated ID that is not allowed as the ID " |
| + "is used for generating a file name. (Related setting: \"" + SETTING_VALIDATION |
| + "\" per \"" + SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID + "\")"); |
| } |
| } |
| } |
| } |
| } |
| |
| private static final String COMMON_TOC_TOPOLOGY_ERROR_HINT |
| = " (Hint: Review the \"" + SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK |
| + "\" setting. Maybe it's incompatible with the structure of " |
| + "this document.)"; |
| |
| private void preprocessDOM_buildTOC_checkTOCTopology(TOCNode tocNode) |
| throws DocgenException { |
| // Check parent-child relation: |
| TOCNode parent = tocNode.getParent(); |
| if (parent != null && !parent.getElement().isSameNode( |
| tocNode.getElement().getParentNode())) { |
| throw new DocgenException( |
| "Bad ToC-element topology: In the ToC " |
| + parent.theSomethingElement() |
| + " is the parent of " |
| + tocNode.theSomethingElement() |
| + ", yet they are not in parent-child relation in the XML " |
| + "document (but maybe in grandparent-nephew relation or " |
| + "like)." |
| + COMMON_TOC_TOPOLOGY_ERROR_HINT); |
| } |
| |
| // Check following-sibling relation: |
| TOCNode next = tocNode.getNext(); |
| Element relevantSibling = preprocessDOM_buildTOC_getSectionLikeSibling( |
| tocNode.getElement(), true); |
| if (next != null) { |
| if (relevantSibling == null) { |
| throw new DocgenException( |
| "Bad ToC-element topology: In the ToC " |
| + next.theSomethingElement() |
| + " is the following sibling of " |
| + tocNode.theSomethingElement() |
| + ", yet they are not siblings in the XML document." |
| + COMMON_TOC_TOPOLOGY_ERROR_HINT); |
| } |
| if (!relevantSibling.isSameNode(next.getElement())) { |
| throw new DocgenException( |
| "Bad ToC-element topology: In the ToC " |
| + next.theSomethingElement() |
| + " is the immediate following sibling of " |
| + tocNode.theSomethingElement() |
| + ", but in the XML document there is a \"" |
| + relevantSibling.getLocalName() |
| + "\" element between them, or they aren't siblings " |
| + "at all." |
| + COMMON_TOC_TOPOLOGY_ERROR_HINT); |
| } |
| } else { |
| // next == null |
| if (relevantSibling != null) { |
| throw new DocgenException( |
| "Bad ToC-element topology: In the ToC hierarchy " |
| + tocNode.theSomethingElement() |
| + "\" is a last-child, but in the XML document it has " |
| + "a \"" + relevantSibling.getLocalName() + "\" " |
| + "element as its following sibling." |
| + COMMON_TOC_TOPOLOGY_ERROR_HINT); |
| } |
| } |
| |
| // Check preceding-sibling relation: |
| TOCNode prev = tocNode.getPrevious(); |
| relevantSibling = preprocessDOM_buildTOC_getSectionLikeSibling( |
| tocNode.getElement(), false); |
| if (prev == null && relevantSibling != null) { |
| throw new DocgenException( |
| "Bad ToC-element topology: In the ToC hierarchy " |
| + tocNode.theSomethingElement() + " is a first-child, " |
| + "but in the XML document it has a " |
| + "\"" + relevantSibling.getLocalName() + "\" " |
| + "element as its preceding sibling." |
| + COMMON_TOC_TOPOLOGY_ERROR_HINT); |
| } |
| |
| TOCNode child = tocNode.getFirstChild(); |
| while (child != null) { |
| preprocessDOM_buildTOC_checkTOCTopology(child); |
| child = child.getNext(); |
| } |
| } |
| |
| private Element preprocessDOM_buildTOC_getSectionLikeSibling( |
| Element elem, boolean next) { |
| Node relevantSibling = elem; |
| do { |
| if (next) { |
| relevantSibling = relevantSibling.getNextSibling(); |
| } else { |
| relevantSibling = relevantSibling.getPreviousSibling(); |
| } |
| } while (relevantSibling != null |
| && !(relevantSibling instanceof Element |
| && DOCUMENT_STRUCTURE_ELEMENTS.contains( |
| relevantSibling.getLocalName()))); |
| return (Element) relevantSibling; |
| } |
| |
| private static final String COMMON_FILE_TOPOLOGY_ERROR_HINT |
| = " (Hint: Review the \"" + SETTING_LOWEST_FILE_ELEMENT_RANK |
| + "\" setting. Maybe it's incompatible with the structure of " |
| + "this document.)"; |
| |
| private void preprocessDOM_buildTOC_checkFileTopology(TOCNode tocNode) |
| throws DocgenException { |
| TOCNode firstChild = tocNode.getFirstChild(); |
| if (firstChild != null) { |
| boolean firstIsFileElement = firstChild.isFileElement(); |
| |
| TOCNode child = firstChild; |
| do { |
| if (child.isFileElement() != firstIsFileElement) { |
| throw new DocgenException("Bad file-element topology: " |
| + "The first child element of " |
| + tocNode.theSomethingElement() |
| + ", " + firstChild.theSomethingElement() |
| + ", is " + (firstIsFileElement ? "a" : "not a") |
| + " file-element, while another child, " |
| + child.theSomethingElement() |
| + (firstIsFileElement ? " isn't" : " is") |
| + ". Either all relevant children elements must be " |
| + "file-elements or neither can be." |
| + COMMON_FILE_TOPOLOGY_ERROR_HINT); |
| } |
| |
| preprocessDOM_buildTOC_checkFileTopology(child); |
| |
| child = child.getNext(); |
| } while (child != null); |
| |
| if (firstIsFileElement && !tocNode.isFileElement()) { |
| throw new DocgenException("Bad file-element topology: " |
| + tocNode.theSomethingElement() + " is not a " |
| + "file-element, yet it has file-element children, " |
| + firstChild.theSomethingElement() + ". Only " |
| + "file-elements can have children that are " |
| + "file-elements."); |
| } |
| } |
| } |
| |
| private TOCNode preprocessDOM_buildTOC_inner(Node node, |
| final int sectionLevel, TOCNode parentTOCNode) |
| throws DocgenException { |
| TOCNode curTOCNode = null; |
| int newSectionLevel = sectionLevel; |
| |
| if (node instanceof Element) { |
| final Element elem = (Element) node; |
| final String nodeName = node.getNodeName(); |
| |
| if (DOCUMENT_STRUCTURE_ELEMENTS.contains(nodeName)) { |
| DocumentStructureRank rank = DocumentStructureRank.valueOf( |
| XMLUtil.getAttribute(elem, A_DOCGEN_RANK) |
| .toUpperCase()); |
| final boolean isTheDocumentElement |
| = elem.getParentNode() instanceof Document; |
| if (isTheDocumentElement |
| || rank.compareTo(lowestPageTOCElemenRank) >= 0) { |
| curTOCNode = new TOCNode(elem, tocNodes.size()); |
| tocNodes.add(curTOCNode); |
| |
| if ((isTheDocumentElement |
| || rank.compareTo(lowestFileElemenRank) >= 0) |
| && !hasPrefaceLikeParent(elem)) { |
| elem.setAttribute(A_DOCGEN_FILE_ELEMENT, "true"); |
| curTOCNode.setFileElement(true); |
| |
| if (isTheDocumentElement) { |
| curTOCNode.setOutputFileName(FILE_TOC_HTML); |
| elem.setAttribute(A_DOCGEN_ROOT_ELEMENT, "true"); |
| } else if (getExternalLinkTOCNodeURLOrNull(elem) != null) { |
| curTOCNode.setOutputFileName(null); |
| } else if (AV_INDEX_ROLE.equals(elem.getAttribute(DocBook5Constants.A_ROLE))) { |
| curTOCNode.setOutputFileName(FILE_INDEX_HTML); |
| } else { |
| String id = XMLUtil.getAttribute(elem, "id"); |
| if (id == null) { |
| throw new BugException("Missing id attribute"); |
| } |
| String fileName = id + ".html"; |
| if (fileName.equals(FILE_TOC_HTML) || fileName.equals(FILE_DETAILED_TOC_HTML) |
| || fileName.equals(FILE_INDEX_HTML) || fileName.equals(FILE_SEARCH_RESULTS_HTML)) { |
| throw new DocgenException( |
| XMLUtil.theSomethingElement(elem, true) |
| + " has an xml:id that is deduced to " |
| + "a reserved output file name, \"" |
| + fileName + "\". (Hint: Change the " |
| + "xml:id.)"); |
| } |
| curTOCNode.setOutputFileName(fileName); |
| } |
| } else { // of: if file element |
| elem.setAttribute(A_DOCGEN_PAGE_TOC_ELEMENT, "true"); |
| } |
| elem.setAttribute(A_DOCGEN_DETAILED_TOC_ELEMENT, "true"); |
| } // if ToC element |
| } // if document structure element |
| } // if Element |
| |
| if (curTOCNode != null) { |
| parentTOCNode = curTOCNode; |
| } |
| |
| NodeList children = node.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| TOCNode child = preprocessDOM_buildTOC_inner( |
| children.item(i), |
| newSectionLevel, |
| parentTOCNode); |
| |
| if (child != null && parentTOCNode != null) { |
| child.setParent(parentTOCNode); |
| TOCNode lastChild = parentTOCNode.getLastChild(); |
| if (lastChild != null) { |
| child.setPrevious(lastChild); |
| lastChild.setNext(child); |
| } |
| |
| if (parentTOCNode.getFirstChild() == null) { |
| parentTOCNode.setFirstChild(child); |
| } |
| parentTOCNode.setLastChild(child); |
| } |
| } |
| |
| return curTOCNode; |
| } |
| |
| private String getExternalLinkTOCNodeURLOrNull(Element elem) throws DocgenException { |
| if (elem.getParentNode() instanceof Document) { |
| // The document element is never an external link ToC node. |
| return null; |
| } |
| |
| Element title = getTitle(elem); |
| if (title == null) { |
| // An element without title can't be an external link ToC node |
| return null; |
| } |
| |
| Iterator<Element> it = XMLUtil.childrenElementsOf(title).iterator(); |
| if (it.hasNext()) { |
| Element firstChild = it.next(); |
| if (!it.hasNext()) { // It's the only child |
| String firstChildName = firstChild.getLocalName(); |
| if (firstChildName.equals(E_LINK)) { |
| String href = XMLUtil.getAttributeNS(firstChild, XMLNS_XLINK, A_XLINK_HREF); |
| if (href == null) { |
| throw new DocgenException(XMLUtil.theSomethingElement(firstChild, true) |
| + " inside a title has no xlink:" + A_XLINK_HREF + " attribute, thus it can't be " |
| + "used as ToC link."); |
| } |
| return href; |
| } else if (firstChildName.equals(E_OLINK)) { |
| String targetdoc = XMLUtil.getAttributeNS(firstChild, null, A_TARGETDOC); |
| if (targetdoc == null) { |
| throw new DocgenException(XMLUtil.theSomethingElement(firstChild, true) |
| + " has no xlink:" + A_TARGETDOC + " attribute"); |
| } |
| String url = olinks.get(targetdoc); |
| if (url == null) { |
| throw new DocgenException(XMLUtil.theSomethingElement(firstChild, true) |
| + " refers to undefined olink name " + StringUtil.jQuote(targetdoc) |
| + "; check configuration."); |
| } |
| return url; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Ensures that |
| * @param tocNodes |
| * @throws DocgenException |
| */ |
| private void preprocessDOM_buildTOC_checkEnsureHasIndexHhml(List<TOCNode> tocNodes) throws DocgenException { |
| for (TOCNode tocNode : tocNodes) { |
| if (tocNode.getOutputFileName() != null && tocNode.getOutputFileName().equals(FILE_INDEX_HTML)) { |
| return; |
| } |
| } |
| // If we had no index.html, the ToC HTML will be renamed to it: |
| for (TOCNode tocNode : tocNodes) { |
| if (tocNode.getOutputFileName() != null && tocNode.getOutputFileName().equals(FILE_TOC_HTML)) { |
| tocNode.setOutputFileName(FILE_INDEX_HTML); |
| return; |
| } |
| } |
| throw new DocgenException( |
| "No " + FILE_INDEX_HTML + " output file would be generated. Add " + DocBook5Constants.A_ROLE + "=\"" |
| + AV_INDEX_ROLE + "\" to one of the elements for which a separate file is generated."); |
| } |
| |
| private boolean hasPrefaceLikeParent(Element elem) { |
| while (true) { |
| Node parent = elem.getParentNode(); |
| if (parent != null && parent instanceof Element) { |
| elem = (Element) parent; |
| if (elem.getNamespaceURI().equals(XMLNS_DOCBOOK5) |
| && PREFACE_LIKE_ELEMENTS.contains( |
| elem.getLocalName())) { |
| return true; |
| } |
| } else { |
| return false; |
| } |
| } |
| } |
| |
| private Element getTitle(Element elem) { |
| NodeList children = elem.getChildNodes(); |
| int ln = children.getLength(); |
| for (int i = 0; i < ln; i++) { |
| Node child = children.item(i); |
| if (child instanceof Element |
| && child.getLocalName().equals("title")) { |
| return (Element) child; // !! found it |
| } |
| } |
| return null; |
| } |
| |
| private void ensureTitleExists(Element elem, String defaultTitle) { |
| if (getTitle(elem) != null) { |
| return; |
| } |
| |
| // Retrieve a document node: |
| Node node = elem; |
| do { |
| node = node.getParentNode(); |
| if (node == null) { |
| throw new BugException("Can't find Document node."); |
| } |
| } while (node.getNodeType() != Node.DOCUMENT_NODE); |
| Document doc = (Document) node; |
| |
| // Create the title node: |
| Element title = doc.createElementNS(XMLNS_DOCBOOK5, E_TITLE); |
| title.appendChild(doc.createTextNode(defaultTitle)); |
| |
| // Insert it into the tree: |
| elem.insertBefore(title, elem.getFirstChild()); |
| } |
| |
| /** |
| * Returns the {@link TOCNode} that corresponds to the element, or |
| * {@link null} if it's not a file element. Can be called only |
| * after {@link #createLookupTables(Node, LookupCreatingState)}. |
| */ |
| private TOCNode getFileTOCNodeFor(Element elem) { |
| for (TOCNode tocNode : tocNodes) { |
| if (tocNode.isFileElement() |
| && tocNode.getElement().isSameNode(elem)) { |
| return tocNode; |
| } |
| } |
| return null; |
| } |
| |
| private void addIndexTerm(Node node) { |
| Node primary = null; |
| Node secondary = null; |
| |
| NodeList children = node.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| Node child = node.getChildNodes().item(i); |
| if (child.getNodeType() == Node.ELEMENT_NODE) { |
| if (child.getNodeName().equals(E_PRIMARY)) { |
| primary = child; |
| } else if (child.getNodeName().equals(E_SECONDARY)) { |
| secondary = child; |
| } |
| } |
| } |
| |
| String primaryText = primary.getFirstChild().getNodeValue().trim(); |
| if (!primaryIndexTermLookup.containsKey(primaryText)) { |
| primaryIndexTermLookup.put(primaryText, new ArrayList<NodeModel>()); |
| } |
| |
| if (secondary != null) { |
| if (!secondaryIndexTermLookup.containsKey(primaryText)) { |
| secondaryIndexTermLookup.put( |
| primaryText, new TreeMap<String, List<NodeModel>>()); |
| } |
| Map<String, List<NodeModel>> m = secondaryIndexTermLookup.get( |
| primaryText); |
| String secondaryText = secondary.getFirstChild().getNodeValue() |
| .trim(); |
| List<NodeModel> nodes = m.get(secondaryText); |
| if (nodes == null) { |
| nodes = new ArrayList<NodeModel>(); |
| m.put(secondaryText, nodes); |
| } |
| nodes.add(NodeModel.wrap(node)); |
| } else { |
| primaryIndexTermLookup.get(primaryText).add(NodeModel.wrap(node)); |
| } |
| } |
| |
| /** |
| * Generates a HTML file for the {@link #currentFileTOCNode}, maybe with |
| * some accompanying HTML-s. |
| */ |
| private int generateHTMLFile() |
| throws IOException, TemplateException { |
| SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); |
| |
| TOCNode otherTOCNode; |
| |
| otherTOCNode = currentFileTOCNode; |
| do { |
| otherTOCNode = otherTOCNode.getPreviousInTraversarOrder(); |
| } while (!(otherTOCNode == null || otherTOCNode.isFileElement())); |
| dataModel.put( |
| VAR_PREVIOUS_FILE_ELEMENT, |
| otherTOCNode != null ? otherTOCNode.getElement() : null); |
| |
| otherTOCNode = currentFileTOCNode; |
| do { |
| otherTOCNode = otherTOCNode.getNextInTraversarOrder(); |
| } while (!(otherTOCNode == null || otherTOCNode.isFileElement())); |
| dataModel.put( |
| VAR_NEXT_FILE_ELEMENT, |
| otherTOCNode != null ? otherTOCNode.getElement() : null); |
| |
| otherTOCNode = currentFileTOCNode.getParent(); |
| dataModel.put( |
| VAR_PARENT_FILE_ELEMENT, |
| otherTOCNode != null ? otherTOCNode.getElement() : null); |
| |
| Element curElem = currentFileTOCNode.getElement(); |
| final boolean isTheDocumentElement |
| = curElem.getParentNode() instanceof Document; |
| dataModel.put( |
| VAR_TOC_DISPLAY_DEPTH, |
| isTheDocumentElement |
| ? maxTOFDisplayDepth : maxMainTOFDisplayDepth); |
| |
| if (seoMeta != null) { |
| Map<String, String> seoMetaMap = seoMeta.get("file:" + currentFileTOCNode.getOutputFileName()); |
| if (seoMetaMap == null) { |
| String id = XMLUtil.getAttribute(currentFileTOCNode.getElement(), "id"); |
| if (id != null) { |
| seoMetaMap = seoMeta.get(id); |
| } |
| } |
| if (seoMetaMap != null) { |
| dataModel.put( |
| VAR_SEO_META_TITLE_OVERRIDE, |
| seoMetaMap.get(SETTING_SEO_META_KEY_TITLE)); |
| dataModel.put( |
| VAR_SEO_META_FULL_TITLE_OVERRIDE, |
| seoMetaMap.get(SETTING_SEO_META_KEY_FULL_TITLE)); |
| dataModel.put( |
| VAR_SEO_META_DESCRIPTION, |
| seoMetaMap.get(SETTING_SEO_META_KEY_DESCRIPTION)); |
| } |
| } |
| |
| boolean generateDetailedTOC = false; |
| if (isTheDocumentElement) { |
| // Find out if a detailed ToC will be useful: |
| int mainTOFEntryCount = countTOFEntries( |
| currentFileTOCNode, maxMainTOFDisplayDepth); |
| if (mainTOFEntryCount != 0 // means, not a single-page output |
| && mainTOFEntryCount < tocNodes.size() * 0.75) { |
| generateDetailedTOC = true; |
| dataModel.put( |
| VAR_ALTERNATIVE_TOC_LINK, |
| FILE_DETAILED_TOC_HTML); |
| dataModel.put( |
| VAR_ALTERNATIVE_TOC_LABEL, |
| "show detailed"); |
| } |
| } |
| |
| generateHTMLFile_inner(dataModel, currentFileTOCNode.getOutputFileName()); |
| |
| if (generateDetailedTOC) { |
| dataModel.put(VAR_PAGE_TYPE, PAGE_TYPE_DETAILED_TOC); |
| dataModel.put( |
| VAR_ALTERNATIVE_TOC_LINK, |
| currentFileTOCNode.getOutputFileName()); |
| dataModel.put( |
| VAR_ALTERNATIVE_TOC_LABEL, |
| "show simplified"); |
| generateHTMLFile_inner(dataModel, FILE_DETAILED_TOC_HTML); |
| return 2; |
| } else { |
| return 1; |
| } |
| } |
| |
| private void generateSearchResultsHTMLFile(Document doc) throws TemplateException, IOException, DocgenException { |
| SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); |
| |
| dataModel.put(VAR_PAGE_TYPE, PAGE_TYPE_SEARCH_RESULTS); |
| dataModel.put(VAR_TOC_DISPLAY_DEPTH, maxMainTOFDisplayDepth); |
| |
| // Create docgen:searchresults element that's no really in the XML file: |
| Element searchresultsElem = doc.createElementNS(XMLNS_DOCGEN, E_SEARCHRESULTS); |
| { |
| // Docgen templates may expect page-elements to have an id: |
| if (elementsById.containsKey(SEARCH_RESULTS_ELEMENT_ID)) { |
| throw new DocgenException("Reserved element id \"" + SEARCH_RESULTS_ELEMENT_ID + "\" was already taken"); |
| } |
| searchresultsElem.setAttribute("id", SEARCH_RESULTS_ELEMENT_ID); |
| |
| searchresultsElem.setAttribute(A_DOCGEN_RANK, E_SECTION); |
| |
| // Docgen templates may expect page-elements to have a title: |
| Element titleElem = doc.createElementNS(XMLNS_DOCBOOK5, E_TITLE); |
| titleElem.setTextContent(SEARCH_RESULTS_PAGE_TITLE); |
| searchresultsElem.appendChild(titleElem); |
| } |
| |
| // We must add it to the document so that .node?root and such will work. |
| doc.getDocumentElement().appendChild(searchresultsElem); |
| try { |
| TOCNode searchresultsTOCNode = new TOCNode(searchresultsElem, 0); |
| searchresultsTOCNode.setFileElement(true); |
| searchresultsTOCNode.setOutputFileName(FILE_SEARCH_RESULTS_HTML); |
| currentFileTOCNode = searchresultsTOCNode; |
| |
| generateHTMLFile_inner(dataModel, currentFileTOCNode.getOutputFileName()); |
| } finally { |
| doc.getDocumentElement().removeChild(searchresultsElem); |
| } |
| } |
| |
| private void generateHTMLFile_inner(SimpleHash dataModel, String fileName) |
| throws TemplateException, IOException { |
| Template template = fmConfig.getTemplate("page.ftlh"); |
| File outputFile = new File(destDir, fileName); |
| FileOutputStream fos = new FileOutputStream(outputFile); |
| OutputStreamWriter osw = new OutputStreamWriter(fos, UTF_8); |
| Writer writer = new BufferedWriter(osw, 2048); |
| try { |
| template.process( |
| dataModel, |
| writer, null, |
| NodeModel.wrap(currentFileTOCNode.getElement())); |
| } finally { |
| writer.close(); |
| } |
| } |
| |
| private int countTOFEntries(TOCNode parent, |
| int displayDepth) { |
| int sum = 0; |
| TOCNode child = parent.getFirstChild(); |
| while (child != null) { |
| if (child.isFileElement()) { |
| sum++; |
| if (displayDepth > 1) { |
| sum += countTOFEntries(child, displayDepth - 1); |
| } |
| } |
| child = child.getNext(); |
| } |
| return sum; |
| } |
| |
| /** |
| * Returns if the TOC node contains anything (other than generated content) in the same file where the TOC node |
| * is displayed. |
| */ |
| private boolean hasContentInTheSameFile(TOCNode tocNode) { |
| for (Element elem : XMLUtil.childrenElementsOf(tocNode.getElement())) { |
| if (elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { |
| String name = elem.getLocalName(); |
| if (!elem.hasAttribute(A_DOCGEN_FILE_ELEMENT) |
| && VISIBLE_TOP_LEVEL_ELEMENTS.contains(name) |
| && !name.equals(E_TITLE) && !name.equals(E_SUBTITLE) |
| && !name.equals(E_INFO) |
| && !name.equals(E_FOOTNOTE)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private String createElementLinkURL(final Element elem) |
| throws DocgenException { |
| if (elem.hasAttribute(A_DOCGEN_NOT_ADDRESSABLE)) { |
| return null; |
| } |
| |
| String extLink = getExternalLinkTOCNodeURLOrNull(elem); |
| if (extLink != null) { |
| return extLink; |
| } |
| |
| // Find the closest id: |
| String id = null; |
| Node node = elem; |
| while (node != null) { |
| if (node instanceof Element) { |
| id = XMLUtil.getAttribute((Element) node, "id"); |
| if (id != null) { |
| break; |
| } |
| } |
| node = node.getParentNode(); |
| } |
| if (id == null) { |
| throw new DocgenException( |
| "Can't create link for the \"" + elem.getLocalName() |
| + "\" element: Nor this element nor its ascendants have an " |
| + "id."); |
| } |
| final Element idElem = (Element) node; |
| |
| String fileName = null; |
| Element curElem = idElem; |
| do { |
| TOCNode fileTOCNode = getFileTOCNodeFor(curElem); |
| if (fileTOCNode == null) { |
| curElem = (Element) curElem.getParentNode(); |
| } else { |
| fileName = fileTOCNode.getOutputFileName(); |
| if (fileName == null) throw new IllegalStateException("fileTOCNode with null outputFileName"); |
| } |
| } while (fileName == null); |
| |
| String link; |
| if (currentFileTOCNode != null |
| && fileName.equals(currentFileTOCNode.getOutputFileName())) { |
| link = ""; |
| } else { |
| link = fileName; |
| } |
| |
| if (getFileTOCNodeFor(idElem) == null) { |
| link = link + "#" + id; |
| } |
| |
| // IE6 doesn't like empty href-s: |
| if (link.length() == 0) { |
| link = fileName; |
| } |
| |
| return link; |
| } |
| |
| private String getArgString(List<?> args, int argIdx) throws TemplateModelException { |
| Object value = args.get(argIdx); |
| if (value instanceof TemplateScalarModel) { |
| return ((TemplateScalarModel) value).getAsString(); |
| } |
| if (value instanceof TemplateModel) { |
| throw new TemplateModelException("Argument #" + (argIdx + 1) + " should be a string, but it was: " |
| + ClassUtil.getFTLTypeDescription((TemplateModel) value)); |
| } |
| throw new IllegalArgumentException("\"value\" must be " + TemplateModel.class.getName()); |
| } |
| |
| private TemplateMethodModelEx createLinkFromID = new TemplateMethodModelEx() { |
| |
| public Object exec(@SuppressWarnings("rawtypes") final List args) |
| throws TemplateModelException { |
| if (args.size() != 1) { |
| throw new TemplateModelException( |
| "Method CreateLinkFromID should have exactly one " |
| + "parameter."); |
| } |
| String id = getArgString(args, 0); |
| |
| try { |
| return createLinkFromId(id); |
| } catch (DocgenException e) { |
| throw new TemplateModelException("Can't resolve id " + StringUtil.jQuote(id) + " to URL", e); |
| } |
| } |
| |
| }; |
| |
| private String createLinkFromId(String id) throws DocgenException { |
| if (elementsById == null) { |
| throw new IllegalStateException("Can't resolve ID as elementsById is still null: " + id); |
| } |
| Element elem = elementsById.get(id); |
| if (elem == null) { |
| throw new DocgenException( |
| "No element exists with this id: \"" + id + "\""); |
| } |
| |
| return createElementLinkURL(elem); |
| } |
| |
| private TemplateMethodModelEx createLinkFromNode |
| = new TemplateMethodModelEx() { |
| |
| public Object exec(@SuppressWarnings("rawtypes") final List args) |
| throws TemplateModelException { |
| |
| if (args.size() != 1) { |
| throw new TemplateModelException( |
| "Method CreateLinkFromNode should have exactly one " |
| + "parameter."); |
| } |
| Object arg1 = args.get(0); |
| if (!(arg1 instanceof NodeModel)) { |
| throw new TemplateModelException( |
| "The first parameter to CreateLinkFromNode must be a " |
| + "node, but it wasn't. (Class: " |
| + arg1.getClass().getName() + ")"); |
| } |
| Node node = ((NodeModel) arg1).getNode(); |
| if (!(node instanceof Element)) { |
| throw new TemplateModelException( |
| "The first parameter to CreateLinkFromNode must be an " |
| + "element node, but it wasn't. (Class: " |
| + arg1.getClass().getName() + ")"); |
| } |
| |
| try { |
| String url = createElementLinkURL((Element) node); |
| return url != null ? new SimpleScalar(url) : null; |
| } catch (DocgenException e) { |
| throw new TemplateModelException( |
| "CreateLinkFromNode falied to create link.", e); |
| } |
| } |
| |
| }; |
| |
| private TemplateMethodModelEx nodeFromID = new TemplateMethodModelEx() { |
| |
| public Object exec(@SuppressWarnings("rawtypes") List args) |
| throws TemplateModelException { |
| Node node = elementsById.get(getArgString(args, 0)); |
| return NodeModel.wrap(node); |
| } |
| |
| }; |
| |
| // ------------------------------------------------------------------------- |
| |
| public File getDestinationDirectory() { |
| return destDir; |
| } |
| |
| /** |
| * Sets the directory where all the output files will go. |
| */ |
| public void setDestinationDirectory(File destDir) { |
| this.destDir = destDir; |
| } |
| |
| public File getSourceDirectory() { |
| return srcDir; |
| } |
| |
| public void setSourceDirectory(File srcDir) { |
| this.srcDir = srcDir; |
| } |
| |
| public Boolean getOffline() { |
| return offline; |
| } |
| |
| public void setOffline(Boolean offline) { |
| this.offline = offline; |
| } |
| |
| public boolean getSimpleNavigationMode() { |
| return simpleNavigationMode; |
| } |
| |
| public void setSimpleNavigationMode(boolean simpleNavigationMode) { |
| this.simpleNavigationMode = simpleNavigationMode; |
| } |
| |
| public boolean getShowEditoralNotes() { |
| return showEditoralNotes; |
| } |
| |
| public void setShowEditoralNotes(boolean showEditoralNotes) { |
| this.showEditoralNotes = showEditoralNotes; |
| } |
| |
| public boolean getValidate() { |
| return validate; |
| } |
| |
| /** |
| * Specifies if the DocBook XML should be validated against the DocBook 5 |
| * RELAX NG Schema; defaults to {@code true}. Setting this to {@code false} |
| * can have whatever random effects later if the DocBook isn't valid, |
| * since the transformation written with the assumption that source is |
| * valid XML. |
| */ |
| public void setValidate(boolean validate) { |
| this.validate = validate; |
| } |
| |
| public TimeZone getTimeZone() { |
| return timeZone; |
| } |
| |
| public void setTimeZone(TimeZone timeZone) { |
| this.timeZone = timeZone; |
| } |
| |
| public boolean getPrintProgress() { |
| return printProgress; |
| } |
| |
| /** |
| * Sets if {@link #execute()} should print feedback to the stdout. |
| * Note that errors (exceptions) will never be printed, just thrown. |
| */ |
| public void setPrintProgress(boolean printProgress) { |
| this.printProgress = printProgress; |
| } |
| |
| public boolean getGenerateEclipseToC() { |
| return generateEclipseTOC; |
| } |
| |
| public void setGenerateEclipseToC(boolean eclipseToC) { |
| this.generateEclipseTOC = eclipseToC; |
| } |
| |
| // ------------------------------------------------------------------------- |
| |
| /** |
| * A node in the XML document for which a ToC entry should be shown. |
| * These nodes form a tree that exists in parallel with the the tree of DOM |
| * nodes. |
| */ |
| public class TOCNode { |
| |
| private final Element element; |
| private final int traversalIndex; |
| private TOCNode parent; |
| private TOCNode next; |
| private TOCNode previous; |
| private TOCNode firstChild; |
| private TOCNode lastChild; |
| private boolean fileElement; |
| private String outputFileName; |
| |
| public TOCNode(Element element, int traversalIndex) { |
| this.element = element; |
| this.traversalIndex = traversalIndex; |
| } |
| |
| public TOCNode getFirstChild() { |
| return firstChild; |
| } |
| |
| public void setFirstChild(TOCNode firstChild) { |
| this.firstChild = firstChild; |
| } |
| |
| public TOCNode getLastChild() { |
| return lastChild; |
| } |
| |
| public void setLastChild(TOCNode lastChild) { |
| this.lastChild = lastChild; |
| } |
| |
| public void setParent(TOCNode parent) { |
| this.parent = parent; |
| } |
| |
| public TOCNode getNext() { |
| return next; |
| } |
| |
| public void setNext(TOCNode next) { |
| this.next = next; |
| } |
| |
| public TOCNode getPrevious() { |
| return previous; |
| } |
| |
| public void setPrevious(TOCNode previous) { |
| this.previous = previous; |
| } |
| |
| public TOCNode getParent() { |
| return parent; |
| } |
| |
| public void setOutputFileName(String outputFileName) { |
| if (!fileElement) { |
| throw new BugException("Can't set outputFileName before setting fileElement to true"); |
| } |
| this.outputFileName = outputFileName; |
| } |
| |
| /** |
| * {@code null} if no file will be generated for this node, despite its "rank". This is the case for nodes that |
| * are external links, or when {@link Transform#simpleNavigationMode} is {@code true} and the file would only |
| * contain a ToC. |
| */ |
| public String getOutputFileName() { |
| return outputFileName; |
| } |
| |
| public Element getElement() { |
| return element; |
| } |
| |
| public void setFileElement(boolean fileElement) { |
| this.fileElement = fileElement; |
| } |
| |
| public boolean isFileElement() { |
| return fileElement; |
| } |
| |
| public String theSomethingElement() { |
| return XMLUtil.theSomethingElement(element); |
| } |
| |
| public TOCNode getNextInTraversarOrder() { |
| return traversalIndex + 1 < tocNodes.size() |
| ? tocNodes.get(traversalIndex + 1) : null; |
| } |
| |
| public TOCNode getPreviousInTraversarOrder() { |
| return traversalIndex > 0 |
| ? tocNodes.get(traversalIndex - 1) : null; |
| } |
| |
| } |
| |
| enum DocumentStructureRank { |
| SIMPLESECT, SECTION3, SECTION2, SECTION1, CHAPTER, PART, BOOK; |
| |
| @Override |
| public String toString() { |
| return name().toLowerCase(); |
| } |
| |
| static String sectionToString(int level) { |
| return DocumentStructureRank.SECTION1.toString().substring( |
| 0, |
| DocumentStructureRank.SECTION1.toString().length() - 1) |
| + level; |
| } |
| } |
| |
| } |