public void execute()

in freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java [524:1225]


    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;

        cfgFile = new File(srcDir, FILE_SETTINGS);
        if (cfgFile.exists()) {
            Map<String, Object> cfg;
            try {
                cfg = CJSONInterpreter.evalAsMap(cfgFile, new DocgenCJSONEvaluationEnvironment(), false);
            } catch (CJSONInterpreter.EvaluationException e) {
                throw new DocgenException(e.getMessage(), e.getCause());
            }

            for (Entry<String, Object> cfgEnt : cfg.entrySet()) {
                final String topSettingName = cfgEnt.getKey();
                final SettingName settingName = SettingName.topLevel(cfgFile, topSettingName);
                final Object settingValue = cfgEnt.getValue();

                if (topSettingName.equals(SETTING_IGNORED_FILES)) {
                    castSettingToList(settingName, settingValue, String.class).forEach(
                            pattern -> ignoredFilePathPatterns.add(FileUtil.globToRegexp(pattern)));
                } else if (topSettingName.equals(SETTING_OLINKS)) {
                    olinks.putAll(
                            castSettingToMap(settingName, settingValue, String.class, String.class));
                } else if (topSettingName.equals(SETTING_INTERNAL_BOOKMARKS)) {
                    internalBookmarks.putAll(
                            castSettingToMap(settingName, settingValue, String.class, String.class));
                    // Book-mark targets will be checked later, when the XML
                    // document is already loaded.
                } else if (topSettingName.equals(SETTING_EXTERNAL_BOOKMARKS)) {
                    externalBookmarks.putAll(
                            castSettingToMap(settingName, settingValue, String.class, String.class));
                } else if (topSettingName.equals(SETTING_LOGO)) {
                    logo = castMapToLogo(settingName, settingValue);
                } else if (topSettingName.equals(SETTING_SIDE_TOC_LOGOS)) {
                    List<Map<String, Object>> listOfMaps = castSetting(
                            settingName, settingValue,
                            List.class,
                            new ListItemType(Map.class),
                            new MapEntryType<>(String.class, Object.class));
                    for (int i = 0; i < listOfMaps.size(); i++) {
                        sideTOCLogos.add(castMapToLogo(settingName.subKey(i), listOfMaps.get(i)));
                    }
                } else if (topSettingName.equals(SETTING_COPYRIGHT_HOLDER)) {
                    copyrightHolder = castSetting(settingName, settingValue, String.class);
                } else if (topSettingName.equals(SETTING_COPYRIGHT_HOLDER_SITE)) {
                    copyrightHolderSite = castSetting(settingName, settingValue, String.class);
                } else if (topSettingName.equals(SETTING_COPYRIGHT_START_YEAR)) {
                    copyrightStartYear = castSetting(settingName, settingValue, Integer.class);
                } else if (topSettingName.equals(SETTING_COPYRIGHT_SUFFIX)) {
                    copyrightSuffix = castSetting(settingName, settingValue, String.class);
                } else if (topSettingName.equals(SETTING_COPYRIGHT_COMMENT_FILE)) {
                    copyrightComment =
                            StringUtil.chomp(getFileContentForSetting(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 (topSettingName.equals(SETTING_SEO_META)) {
                    this.seoMeta.putAll(
                            castSetting(
                                    settingName, settingValue,
                                    Map.class,
                                    new MapEntryType<>(String.class, Map.class),
                                    new MapEntryType<>(
                                            String.class, Collections.emptySet(), SETTING_SEO_META_KEYS,
                                            String.class)));
                } else if (topSettingName.equals(SETTING_CUSTOM_VARIABLES)) {
                    customVariablesFromSettingsFile.putAll(
                            // Allow null values in the Map, as the caller can override them.
                            castSettingToMap(settingName, settingValue, String.class, Object.class, true));
                } else if (topSettingName.equals(SETTING_INSERTABLE_FILES)) {
                    insertableFilesFromSettingsFile.putAll(
                            // Allow null values in the Map, as the caller can override them.
                            castSettingToMap(settingName, settingValue, String.class, String.class, true));
                } else if (topSettingName.equals(SETTING_INSERTABLE_OUTPUT_COMMANDS)) {
                    Map<String, Map<String, Object>> m = castSetting(
                            settingName, settingValue,
                            Map.class,
                            new MapEntryType(String.class, Map.class),
                            new MapEntryType(
                                    String.class, SETTING_INSERTABLE_OUTPUT_COMMANDS_REQUIRED_KEYS, SETTING_INSERTABLE_OUTPUT_COMMANDS_OPTIONAL_KEYS,
                                    Object.class, false));
                    for (Entry<String, Map<String, Object>> ent : m.entrySet()) {
                        String commandKey = ent.getKey();
                        Map<String, Object> outputCmdProps = ent.getValue();
                        InsertableOutputCommandProperties commandProps = new InsertableOutputCommandProperties(
                                castSetting(
                                        settingName.subKey(commandKey,
                                                SETTING_INSERTABLE_OUTPUT_COMMANDS_MAIN_CLASS_KEY),
                                        outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMANDS_MAIN_CLASS_KEY),
                                        String.class
                                ),
                                castSetting(
                                        settingName.subKey(commandKey,
                                                SETTING_INSERTABLE_OUTPUT_COMMANDS_MAIN_METHOD_KEY),
                                        outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMANDS_MAIN_METHOD_KEY),
                                        String.class
                                ),
                                castSetting(
                                        settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMANDS_SYSTEM_PROPERTIES_KEY),
                                        outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMANDS_SYSTEM_PROPERTIES_KEY),
                                        new DefaultValue<>(Collections.emptyMap()),
                                        Map.class, new MapEntryType(String.class, String.class)
                                ),
                                castSetting(
                                        settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMANDS_PREPENDED_ARGUMENTS_KEY),
                                        outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMANDS_PREPENDED_ARGUMENTS_KEY),
                                        new DefaultValue<>(Collections.emptyList()),
                                        List.class
                                ),
                                castSetting(
                                        settingName.subKey(commandKey, SETTING_INSERTABLE_OUTPUT_COMMANDS_APPENDED_ARGUMENTS_KEY),
                                        outputCmdProps.get(SETTING_INSERTABLE_OUTPUT_COMMANDS_APPENDED_ARGUMENTS_KEY),
                                        new DefaultValue<>(Collections.emptyList()),
                                        List.class
                                ),
                                Optional.ofNullable(
                                        SettingUtils.<String>castSetting( // Explicit generic type to dodge JDK 8 252 bug
                                                settingName.subKey(commandKey,
                                                        SETTING_INSERTABLE_OUTPUT_COMMANDS_DOCGEN_WD_REPLACED_WITH_KEY),
                                                outputCmdProps.get(
                                                        SETTING_INSERTABLE_OUTPUT_COMMANDS_DOCGEN_WD_REPLACED_WITH_KEY),
                                                DefaultValue.NULL,
                                                String.class
                                        )
                                ).map(it -> Paths.get(it).toAbsolutePath().normalize()).orElse(null)
                        );
                        insertableOutputCommands.put(commandKey, commandProps);
                    }
                } else if (topSettingName.equals(SETTING_TABS)) {
                    tabs.putAll(
                            castSettingToMap(settingName, settingValue, String.class, String.class));
                } else if (topSettingName.equals(SETTING_SECONDARY_TABS)) {
                    secondaryTabs.putAll(
                            castSetting(
                                    settingName, settingValue,
                                    Map.class,
                                    new MapEntryType(String.class, Map.class),
                                    new MapEntryType(String.class, COMMON_LINK_KEYS, String.class)));
                } else if (topSettingName.equals(SETTING_SOCIAL_LINKS)) {
                    socialLinks.putAll(
                            castSetting(
                                    settingName, settingValue,
                                    Map.class,
                                    new MapEntryType(String.class, Map.class),
                                    new MapEntryType(String.class, COMMON_LINK_KEYS, String.class)));
                } else if (topSettingName.equals(SETTING_FOOTER_SITEMAP)) {
                    footerSiteMap.putAll(
                            castSetting(
                                    settingName, settingValue,
                                    Map.class,
                                    new MapEntryType(String.class, Map.class),
                                    new MapEntryType(String.class, String.class)));
                }else if (topSettingName.equals(SETTING_VALIDATION)) {
                    castSettingToMap(settingName, settingValue, String.class, Object.class)
                            .forEach((name, value) -> {
                                if (name.equals(
                                        SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE)) {
                                    validationOps.setProgramlistingRequiresRole(
                                            castSetting(settingName.subKey(name), value, Boolean.class));
                                } else if (name.equals(
                                        SETTING_VALIDATION_PROGRAMLISTINGS_REQ_LANG)) {
                                    validationOps.setProgramlistingRequiresLanguage(
                                            castSetting(settingName.subKey(name), value, Boolean.class));
                                } else if (name.equals(
                                        SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID)
                                ) {
                                    validationOps.setOutputFilesCanUseAutoID(
                                            castSetting(settingName.subKey(name), value, Boolean.class));
                                } else if (name.equals(
                                        SETTING_VALIDATION_MAXIMUM_PROGRAMLISTING_WIDTH)
                                ) {
                                    validationOps.setMaximumProgramlistingWidth(
                                            castSetting(settingName.subKey(name), value, Integer.class));
                                } else {
                                    throw newCfgFileException(settingName.subKey(name), "Unknown validation option: " + name);
                                }
                            });
                } else if (topSettingName.equals(SETTING_OFFLINE)) {
                    if (offline == null) {  // Ignore if the caller has already set this
                        offline = castSetting(settingName, settingValue, Boolean.class);
                    }
                } else if (topSettingName.equals(SETTING_SIMPLE_NAVIGATION_MODE)) {
                    simpleNavigationMode = castSetting(settingName, settingValue, Boolean.class);
                } else if (topSettingName.equals(SETTING_DEPLOY_URL)) {
                    deployUrl = castSetting(settingName, settingValue, String.class);
                } else if (topSettingName.equals(SETTING_ONLINE_TRACKER_HTML)) {
                    onlineTrackerHTML = getFileContentForSetting(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 (topSettingName.equals(SETTING_COOKIE_CONSENT_SCRIPT_URL)) {
                    cookieConstentScriptURL = castSetting(settingName, settingValue, String.class);
                } else if (topSettingName.equals(SETTING_REMOVE_NODES_WHEN_ONLINE)) {
                    removeNodesWhenOnline = Collections.unmodifiableSet(new HashSet<>(
                            castSettingToList(settingName, settingValue, String.class)));
                } else if (topSettingName.equals(SETTING_ECLIPSE)) {
                    castSettingToMap(settingName, settingValue, String.class, Object.class)
                            .forEach((name, value) -> {
                                if (name.equals(SETTING_ECLIPSE_LINK_TO)) {
                                    eclipseLinkTo = castSetting(
                                            settingName.subKey(name), value, String.class);
                                } else {
                                    throw newCfgFileException(settingName, "Unknown Eclipse option: " + name);
                                }
                            });
                } else if (topSettingName.equals(SETTING_LOCALE)) {
                    String s = castSetting(settingName, settingValue, String.class);
                    locale = StringUtil.deduceLocale(s);
                } else if (topSettingName.equals(SETTING_TIME_ZONE)) {
                    String s = castSetting(settingName, settingValue, String.class);
                    timeZone = TimeZone.getTimeZone(s);
                } else if (topSettingName.equals(SETTING_GENERATE_ECLIPSE_TOC)) {
                    generateEclipseTOC = castSetting(settingName, settingValue, Boolean.class);
                } else if (topSettingName.equals(SETTING_SHOW_EDITORAL_NOTES)) {
                    showEditoralNotes = castSetting(settingName, settingValue, Boolean.class);
                } else if (topSettingName.equals(SETTING_SHOW_XXE_LOGO)) {
                    showXXELogo = castSetting(settingName, settingValue, Boolean.class);
                } else if (topSettingName.equals(SETTING_SEARCH_KEY)) {
                    searchKey = castSetting(settingName, settingValue, String.class);
                }else if (topSettingName.equals(SETTING_DISABLE_JAVASCRIPT)) {
                    disableJavaScript = castSetting(settingName, settingValue, Boolean.class);
                } else if (topSettingName.equals(SETTING_CONTENT_DIRECTORY)) {
                    String s = castSetting(settingName, settingValue, String.class);
                    contentDir = new File(srcDir, s);
                    if (!contentDir.isDirectory()) {
                        throw newCfgFileException(
                                settingName,
                                "It's not an existing directory: " + contentDir.getAbsolutePath());
                    }
                } else if (topSettingName.equals(SETTING_LOWEST_FILE_ELEMENT_RANK)
                        || topSettingName.equals(SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) {
                    DocumentStructureRank rank;
                    String strRank = castSetting(settingName, settingValue, String.class);
                    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(settingName, msg);
                    }

                    if (topSettingName.equals(SETTING_LOWEST_FILE_ELEMENT_RANK)) {
                        lowestFileElemenRank = rank;
                    } else if (topSettingName.equals(SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) {
                        lowestPageTOCElemenRank = rank;
                    } else {
                        throw new BugException("Unexpected setting name.");
                    }
                } else if (topSettingName.equals(SETTING_MAX_TOF_DISPLAY_DEPTH)) {
                    maxTOFDisplayDepth = castSetting(settingName, settingValue, Integer.class);
                    if (maxTOFDisplayDepth < 1) {
                        throw newCfgFileException(settingName, "Value must be at least 1.");
                    }
                } else if (topSettingName.equals(SETTING_MAX_MAIN_TOF_DISPLAY_DEPTH)) {
                    maxMainTOFDisplayDepth = castSetting(settingName, settingValue, Integer.class);
                    if (maxTOFDisplayDepth < 1) {
                        throw newCfgFileException(settingName, "Value must be at least 1.");
                    }
                } else if (topSettingName.equals(SETTING_NUMBERED_SECTIONS)) {
                    numberedSections = castSetting(settingName, settingValue, Boolean.class);
                } else {
                    throw newCfgFileException(settingName, "Unknown setting name.");
                }
            } // 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<>();
        secondaryIndexTermLookup = new HashMap<>();
        elementsById = new HashMap<>();
        tocNodes = new ArrayList<>();
        indexEntries = new ArrayList<>();

        // 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()));
            }
        }
        for (Map<String, String> secondaryTab : secondaryTabs.values()) {
            secondaryTab.put("href", resolveDocgenURL(SETTING_SECONDARY_TABS, secondaryTab.get("href")));
        }
        if (externalBookmarks != null) {
            for (Entry<String, String> bookmarkEnt : externalBookmarks.entrySet()) {
                bookmarkEnt.setValue(resolveDocgenURL(SETTING_EXTERNAL_BOOKMARKS, bookmarkEnt.getValue()));
            }
        }
        for (Map<String, String> tab : socialLinks.values()) {
            tab.put("href", resolveDocgenURL(SETTING_SOCIAL_LINKS, tab.get("href")));
        }
        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);
        }
        for (Logo logo : sideTOCLogos) {
            resolveLogoHref(logo);
        }

        // - Create destination directory:
        if (!destDir.isDirectory() && !destDir.mkdirs()) {
            throw new IOException("Failed to create destination directory: "
                    + destDir.getAbsolutePath());
        }
        logger.info("Output directory: " + destDir.getAbsolutePath());

        // - Check internal book-marks:
        for (Entry<String, String> ent : internalBookmarks.entrySet()) {
            String id = ent.getValue();
            if (!elementsById.containsKey(id)) {
                throw newCfgFileException(
                        SettingName.topLevel(cfgFile, SETTING_INTERNAL_BOOKMARKS),
                        "No element with id \"" + id + "\" exists in the book.");
            }
        }

        insertableFiles = computeInsertableFiles();

        // - 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());
            fmConfig.setSharedVariable(
                    VAR_CUSTOM_VARIABLES, computeCustomVariables());

            fmConfig.setSharedVariable(
                    "printTextWithDocgenSubstitutions",
                    new PrintTextWithDocgenSubstitutionsDirective(this));
            fmConfig.setSharedVariable(
                    "chopLinebreak",
                    ChopLinebreakDirective.INSTANCE);

            // 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 = FileUtil.newFileWriter(new File(destDir, FILE_TOC_JSON_OUTPUT))) {
                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 = FileUtil.newFileWriter(new File(destDir, FILE_SITEMAP_XML_OUTPUT))) {
                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 (DocgenTagException e) {
                    throw new DocgenException("Docgen tag evaluation in document text failed; see cause exception", e);
                } 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 = FileUtil.newFileWriter(new File(destDir, FILE_ECLIPSE_TOC_OUTPUT))) {
                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" : ""));
    }