TldDocGenerator.java

/*
 * <license>
 * Copyright (c) 2003-2004, Sun Microsystems, Inc.
 * Copyright (c) 2022-2026, Web-Legacy
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Sun Microsystems, Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * </license>
 */

package io.github.weblegacy.tlddoc.main;

import io.github.weblegacy.tlddoc.Constants;
import io.github.weblegacy.tlddoc.JarTldFileTagLibrary;
import io.github.weblegacy.tlddoc.TagDirImplicitTagLibrary;
import io.github.weblegacy.tlddoc.TldFileTagLibrary;
import io.github.weblegacy.tlddoc.Utils;
import io.github.weblegacy.tlddoc.WarJarTldFileTagLibrary;
import io.github.weblegacy.tlddoc.WarTagDirImplicitTagLibrary;
import io.github.weblegacy.tlddoc.tagfileparser.Attribute;
import io.github.weblegacy.tlddoc.tagfileparser.Directive;
import io.github.weblegacy.tlddoc.tagfileparser.javacc.ParseException;
import io.github.weblegacy.tlddoc.tagfileparser.javacc.TagFile;
import java.io.CharArrayReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * TldDoc Generator. Takes a set of TLD files and generates a set of javadoc-style HTML pages that
 * describe the various components of the input tag libraries.
 *
 * @author Mark Roth
 */
public class TldDocGenerator {

    /**
     * The set of tag libraries we are parsing. Each element is a TagLibrary instance.
     */
    private final ArrayList<TagLibrary> tagLibraries = new ArrayList<>();

    /**
     * The directory containing the stylesheets, or null if the default stylesheets are to be used.
     */
    private Path xsltDirectory = null;

    /**
     * The output directory for generated files.
     */
    private Path outputDirectory = Paths.get("out");

    /**
     * The browser window title for the documentation.
     */
    private String windowTitle = Constants.DEFAULT_WINDOW_TITLE;

    /**
     * The title for the TLD index (first) page.
     */
    private String docTitle = Constants.DEFAULT_DOC_TITLE;

    /**
     * {@code True} if no {@code stdout} is to be produced during generation.
     */
    private boolean quiet;

    /**
     * {@code True} if all tld-conversions output is to be produced during generation.
     */
    private boolean verbose;

    /**
     * The summary TLD document, used as input into XSLT.
     */
    private Document summaryTld;

    /**
     * Path to tlddoc resources.
     */
    private static final String RESOURCE_PATH = "/io/github/weblegacy/tlddoc/resources";

    /**
     * Helps uniquely generate substitute prefixes in the case of missing or duplicate short-names.
     */
    private int substitutePrefix = 1;

    /**
     * Creates a new TldDocGenerator.
     */
    public TldDocGenerator() {
    }

    /**
     * Adds the given Tag Library to the list of Tag Libraries to generate documentation for.
     *
     * @param tagLibrary The tag library to add.
     */
    public void addTagLibrary(TagLibrary tagLibrary) {
        tagLibraries.add(tagLibrary);
    }

    /**
     * Adds the given individual TLD file.
     *
     * @param tld The TLD file to add
     */
    public void addTld(Path tld) {
        addTagLibrary(new TldFileTagLibrary(tld));
    }

    /**
     * Adds all the tag libraries found in the given web application.
     *
     * @param path The path to the root of the web application.
     */
    public void addWebApp(Path path) {
        try {
            Path webinf = path.endsWith("WEB-INF") ? path : path.resolve("WEB-INF");

            // Scan all subdirectories of /WEB-INF/ for .tld files
            addWebAppTldsIn(webinf);

            // Add all JAR files in /WEB-INF/lib that might potentially
            // contain TLDs.
            addWebAppJarsIn(webinf.resolve("lib"));

            // Add all implicit tag libraries in /WEB-INF/tags
            addWebAppTagDirsIn(webinf.resolve("tags"));

        } catch (IOException e) {
            println("WARNING: Could not access one or more entries in " + path.toAbsolutePath()
                    + ".  Skipping Web-App.  Reason: " + e.getMessage());
        }
    }

    /**
     * Adds all TLD files under the given directory, recursively.
     *
     * @param path The path to search (recursively) for TLDs in.
     *
     * @throws IOException if an I/O error has occurred
     */
    private void addWebAppTldsIn(Path path) throws IOException {
        Utils.processFiles(path, Utils::isTld, this::addTld);
    }

    /**
     * Adds all TLD files under the given directory, recursively.
     *
     * @param war  The WAR file to search
     * @param path The path to search (recursively) for TLDs in.
     *
     * @throws IOException if an I/O error has occurred
     */
    private void addWarTldsIn(Path war, String path) throws IOException {
        try (JarFile warFile = new JarFile(war.toFile())) {
            final Enumeration<JarEntry> entries = warFile.entries();
            while (entries.hasMoreElements()) {
                final JarEntry jarEntry = entries.nextElement();
                final String entryName = jarEntry.getName();
                if (entryName.startsWith(path) && Utils.isTld(entryName)) {
                    addTagLibrary(new JarTldFileTagLibrary(war, jarEntry.getName()));
                }
            }
        }
    }

    /**
     * Adds all JAR files under the given directory, recursively.
     *
     * @param path The path to search (recursively) for JARs in.
     *
     * @throws IOException if an I/O error has occurred
     */
    private void addWebAppJarsIn(Path path) throws IOException {
        Utils.processFiles(path, Utils::isJar, this::addJar);
    }

    /**
     * Adds all JAR files under the given directory, recursively.
     *
     * @param war  The WAR file to search
     * @param path The path to search (recursively) for JARs in.
     *
     * @throws IOException if an I/O error has occurred
     */
    private void addWarJarsIn(Path war, String path) throws IOException {
        try (JarFile warFile = new JarFile(war.toFile())) {
            Enumeration<JarEntry> entries = warFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry warEntry = entries.nextElement();
                String entryName = warEntry.getName();
                if (entryName.startsWith(path) && Utils.isJar(entryName)) {
                    // Add all tag libraries found in the given JAR file that is
                    // inside this WAR file:
                    try (JarInputStream in = new JarInputStream(warFile.getInputStream(warEntry))) {
                        // Search for all TLD files in the JAR file

                        JarEntry jarEntry;
                        while ((jarEntry = in.getNextJarEntry()) != null) {
                            if (Utils.isTld(jarEntry.getName())) {
                                addTagLibrary(new WarJarTldFileTagLibrary(war,
                                        entryName, jarEntry.getName()));
                            }
                        }
                    } catch (IOException e) {
                        println("WARNING: Could not access one or more entries in "
                                + war.toAbsolutePath() + " entry " + entryName
                                + ".  Skipping JAR.  Reason: " + e.getMessage());
                    }
                }
            }
        }
    }

    /**
     * Adds all implicit tag libraries under the given directory, recursively.
     *
     * @param path The path to search (recursively) for tag file directories in
     *
     * @throws IOException if an I/O error has occurred
     */
    private void addWebAppTagDirsIn(Path path) throws IOException {
        Utils.processDirs(path, this::addTagDir);
    }

    /**
     * Adds all implicit tag libraries under the given directory, recursively.
     *
     * @param war  The WAR file to search
     * @param path The path to search (recursively) for tag file directories in
     *
     * @throws IOException if an I/O error has occurred
     */
    private void addWarTagDirsIn(Path war, String path) throws IOException {
        try (JarFile warFile = new JarFile(war.toFile())) {
            Enumeration<JarEntry> entries = warFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.getName().startsWith(path) && entry.isDirectory()) {
                    addTagLibrary(new WarTagDirImplicitTagLibrary(war, entry.getName()));
                }
            }
        }
    }

    /**
     * Adds all the tag libraries found in the given JAR.
     *
     * @param jar The JAR file to add.
     */
    public void addJar(Path jar) {
        try (JarFile jarFile = new JarFile(jar.toFile())) {
            // Search for all TLD files in the JAR file
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                if (Utils.isTld(jarEntry.getName())) {
                    addTagLibrary(new JarTldFileTagLibrary(jar, jarEntry.getName()));
                }
            }
        } catch (IOException e) {
            println("WARNING: Could not access one or more entries in " + jar.toAbsolutePath()
                    + ".  Skipping JAR.  Reason: " + e.getMessage());
        }
    }

    /**
     * Adds all the tag libraries found in the given web application packaged as a WAR file.
     *
     * @param path The war containing the web application
     */
    public void addWar(Path path) {
        try {
            // Scan all subdirectories of /WEB-INF/ for .tld files
            addWarTldsIn(path, "WEB-INF/");

            // Add all JAR files in /WEB-INF/lib that might potentially
            // contain TLDs.
            addWarJarsIn(path, "WEB-INF/lib/");

            // Add all implicit tag libraries in /WEB-INF/tags
            addWarTagDirsIn(path, "WEB-INF/tags/");
        } catch (IOException e) {
            println("WARNING: Could not access one or more entries in " + path.toAbsolutePath()
                    + ".  Skipping WAR.  Reason: " + e.getMessage());
        }
    }

    /**
     * Adds the given directory of tag files.
     *
     * @param tagdir The tag directory to add
     */
    public void addTagDir(Path tagdir) {
        addTagLibrary(new TagDirImplicitTagLibrary(tagdir));
    }

    /**
     * Sets the directory from which to obtain the XSLT stylesheets. This allows the user to change
     * the look and feel of the generated output. If not specified, the default stylesheets are
     * used.
     *
     * @param dir The base directory for the stylesheets
     */
    public void setXsltDirectory(Path dir) {
        this.xsltDirectory = dir;
    }

    /**
     * Sets the output directory for generated files. If not specified, defaults to "."
     *
     * @param dir The base directory for generated files.
     */
    public void setOutputDirectory(Path dir) {
        this.outputDirectory = dir;
    }

    /**
     * Sets the browser window title for the documentation.
     *
     * @param title The browser window title
     */
    public void setWindowTitle(String title) {
        this.windowTitle = title;
    }

    /**
     * Sets the title for the TLD index (first) page.
     *
     * @param title The title for the TLD index
     */
    public void setDocTitle(String title) {
        this.docTitle = title;
    }

    /**
     * Sets quiet mode (produce no {@code stdout} during generation).
     *
     * @param quiet {@code True} if no output is to be produced, {@code false} otherwise.
     */
    public void setQuiet(boolean quiet) {
        this.quiet = quiet;
    }

    /**
     * Returns {@code true} if the generator is in quiet mode or {@code false} if not.
     *
     * @return {@code True} if no output is to be produced, {@code false} otherwise.
     */
    public boolean isQuiet() {
        return quiet;
    }

    /**
     * Sets verbose mode (produce {@code stdout} with all tld-conversions during generation).
     *
     * @param verbose {@code true} if all tld-conversions output is to be produced, {@code false}
     *                otherwise.
     */
    public void setVerbose(boolean verbose) {
        this.verbose = verbose;
    }

    /**
     * Returns {@code true} if the generator is in verbose mode or {@code false} if not.
     *
     * @return {@code True} if all tld-conversions output is to be produced, {@code false}
     *         otherwise.
     */
    public boolean isVerbose() {
        return verbose;
    }

    /**
     * Commences documentation generation.
     *
     * @throws GeneratorException any error during generation
     */
    public void generate() throws GeneratorException {
        try {
            Files.createDirectories(outputDirectory);

            copyStaticFiles();
            createTldSummaryDoc();
            generateOverview();
            generateTldDetail();
            outputSuccessMessage();
        } catch (IOException | SAXException | TransformerFactoryConfigurationError
                | FactoryConfigurationError | ParserConfigurationException
                | TransformerException e) {
            throw new GeneratorException(e);
        }
    }

    // //////////////////////////////////////////////////////////////////
    /**
     * Copies all static files to target directory.
     *
     * @throws IOException if an I/O error has occurred
     */
    private void copyStaticFiles() throws IOException {
        copyResourceToFile(outputDirectory.resolve("stylesheet.css"),
                RESOURCE_PATH + "/stylesheet.css");
    }

    /**
     * Creates a summary document, comprising all input TLDs. This document is later used as input
     * into XSLT to generate all non-static output pages. Stores the result as a DOM tree in the
     * summaryTLD attribute.
     *
     * @throws IOException                          if an I/O error has occurred
     * @throws SAXException                         If any parse errors occur.
     * @throws TransformerFactoryConfigurationError Thrown in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws TransformerConfigurationException    Thrown if there are errors when parsing the
     *                                              {@code Source} or it is not possible to create a
     *                                              {@code Transformer} instance.
     * @throws FactoryConfigurationError            in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws ParserConfigurationException         if a DocumentBuilder cannot be created which
     *                                              satisfies the configuration requested.
     * @throws TransformerException                 If an unrecoverable error occurs during the
     *                                              course of the transformation.
     * @throws GeneratorException                   taglib is not valid
     */
    private void createTldSummaryDoc() throws IOException, SAXException,
            TransformerFactoryConfigurationError, TransformerConfigurationException,
            FactoryConfigurationError, ParserConfigurationException, TransformerException,
            GeneratorException {

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(false);
        factory.setNamespaceAware(true);
        factory.setExpandEntityReferences(false);
        DocumentBuilder documentBuilder = factory.newDocumentBuilder();
        documentBuilder.setEntityResolver((publicId, systemId)
                -> new InputSource(new CharArrayReader(new char[0]))
        );
        summaryTld = documentBuilder.newDocument();

        // Create root <tlds> element:
        Element rootElement = summaryTld.createElementNS(Constants.NS_JAKARTAEE, "tlds");
        summaryTld.appendChild(rootElement);
        // JDK 1.4 does not add xmlns for some reason - add it manually:
        rootElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns",
                Constants.NS_JAKARTAEE);

        // Create configuration element:
        Element configElement = summaryTld.createElementNS(Constants.NS_JAKARTAEE, "config");
        rootElement.appendChild(configElement);

        Element windowTitleElement = summaryTld.createElementNS(Constants.NS_JAKARTAEE,
                "window-title");
        windowTitleElement.appendChild(summaryTld.createTextNode(this.windowTitle));
        configElement.appendChild(windowTitleElement);

        Element docTitleElement = summaryTld.createElementNS(Constants.NS_JAKARTAEE, "doc-title");
        docTitleElement.appendChild(summaryTld.createTextNode(this.docTitle));
        configElement.appendChild(docTitleElement);

        // Append each <taglib> element from each TLD:
        println("Loading and translating " + tagLibraries.size()
                + " Tag Librar"
                + ((tagLibraries.size() == 1) ? "y" : "ies")
                + "...");
        for (final TagLibrary tagLibrary_ : tagLibraries) {
            // to AutoClose internal files at TagLibrary-Implementations
            try (TagLibrary tagLibrary = tagLibrary_) {
                Document doc = tagLibrary.getTldDocument(documentBuilder);

                // Convert document to JSP 4.0 TLD
                doc = upgradeTld(doc);

                // If this tag library has no tags, no validators,
                // and no functions, omit it
                final Element element = doc.getDocumentElement();
                int numTags = element == null ? 0
                        : element.getElementsByTagNameNS("*", "tag").getLength()
                        + element.getElementsByTagNameNS("*", "tag-file").getLength()
                        + element.getElementsByTagNameNS("*", "validator").getLength()
                        + element.getElementsByTagNameNS("*", "function").getLength();
                if (numTags > 0) {
                    // Populate the root element with extra information
                    populateTld(tagLibrary, doc);

                    Element taglibNode = (Element) summaryTld.importNode(
                            doc.getDocumentElement(), true);
                    if (!(taglibNode.getNamespaceURI().equals(Constants.NS_JAKARTAEE)
                            || taglibNode.getNamespaceURI().equals(Constants.NS_JAVAEE)
                            || taglibNode.getNamespaceURI().equals(Constants.NS_J2EE))) {
                        throw new GeneratorException("Error: "
                                + tagLibrary.getPathDescription()
                                + " does not have xmlns=\"" + Constants.NS_JAKARTAEE + "\"");
                    }
                    if (!taglibNode.getLocalName().equals("taglib")) {
                        throw new GeneratorException("Error: "
                                + tagLibrary.getPathDescription()
                                + " does not have <taglib> as root.");
                    }
                    rootElement.appendChild(taglibNode);
                }
            }
        }

        // If debug enabled, output the resulting document, as a test:
        if (Constants.DEBUG_INPUT_DOCUMENT) {
            Transformer transformer
                    = TransformerFactory.newInstance().newTransformer();
            transformer.transform(new DOMSource(summaryTld),
                    new StreamResult(System.out));
        }
    }

    /**
     * Converts the given TLD to a JSP 4.0 TLD.
     *
     * @param doc the given TLD
     *
     * @return the converted to JSP 4.0 TLD
     *
     * @throws TransformerFactoryConfigurationError Thrown in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws TransformerConfigurationException    Thrown if there are errors when parsing the
     *                                              {@code Source} or it is not possible to create a
     *                                              {@code Transformer} instance.
     * @throws FactoryConfigurationError            in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws ParserConfigurationException         if a DocumentBuilder cannot be created which
     *                                              satisfies the configuration requested.
     * @throws TransformerException                 If an unrecoverable error occurs during the
     *                                              course of the transformation.
     */
    private Document upgradeTld(Document doc) throws
            TransformerFactoryConfigurationError, TransformerConfigurationException,
            FactoryConfigurationError, ParserConfigurationException, TransformerException {

        Element root = doc.getDocumentElement();

        // We use getElementsByTagName instead of getElementsByTagNameNS
        // here since JSP 1.1 TLDs have no namespace.
        if (root.getElementsByTagName("jspversion").getLength() > 0) {
            removeNameSpace(doc, root);

            // JSP 1.1 TLD - convert to JSP 1.2 TLD first.
            doc = convertTld(doc, RESOURCE_PATH + "/tld1_1-tld1_2.xsl");
            root = doc.getDocumentElement();
        }

        // We use getElementsByTagName instead of getElementsByTagNameNS
        // here since JSP 1.2 TLDs have no namespace.
        if (root.getElementsByTagName("jsp-version").getLength() > 0) {
            removeNameSpace(doc, root);

            // JSP 1.2 TLD - convert to JSP 2.0 TLD first
            doc = convertTld(doc, RESOURCE_PATH + "/tld1_2-tld2_0.xsl");
            root = doc.getDocumentElement();
        }

        if ("2.0".equals(root.getAttribute("version"))) {
            // JSP 2.0 TLD - convert to JSP 2.1 TLD first
            doc = convertTld(doc, RESOURCE_PATH + "/tld2_0-tld2_1.xsl");
            root = doc.getDocumentElement();
        }

        if ("2.1".equals(root.getAttribute("version"))) {
            // JSP 2.1 TLD - convert to JSP 3.0 TLD first
            doc = convertTld(doc, RESOURCE_PATH + "/tld2_1-tld3_0.xsl");
            root = doc.getDocumentElement();
        }

        if ("3.0".equals(root.getAttribute("version"))) {
            // JSP 3.0 TLD - convert to JSP 3.1 TLD first
            doc = convertTld(doc, RESOURCE_PATH + "/tld3_0-tld3_1.xsl");
            root = doc.getDocumentElement();
        }

        if ("3.1".equals(root.getAttribute("version"))) {
            // JSP 3.1 TLD - convert to JSP 4.0 TLD first
            doc = convertTld(doc, RESOURCE_PATH + "/tld3_1-tld4_0.xsl");
        }

        // Final conversion to remove unwanted elements
        doc = convertTld(doc, RESOURCE_PATH + "/tld4_0-tld4_0.xsl");

        // We should now have a JSP 4.0 TLD in doc.
        return doc;
    }

    /**
     * Deletes a possibly existing namespace from the root element.
     *
     * @param doc  the document
     * @param root the root-element
     */
    private static void removeNameSpace(final Document doc, final Element root) {
        final String ns = root.getNamespaceURI();
        if (!(ns == null || ns.isEmpty())) {
            removeNameSpace(doc, root, ns);
        }
    }

    /**
     * Deletes a specific namespace from all elements and attributes.
     *
     * @param doc  the document
     * @param node the current element
     * @param ns   the namespace to remove
     */
    private static void removeNameSpace(final Document doc, final Node node, final String ns) {
        if ((node.getNodeType() == Node.ELEMENT_NODE || node.getNodeType() == Node.ATTRIBUTE_NODE)
                && ns.equals(node.getNamespaceURI())) {
            doc.renameNode(node, null, node.getLocalName());
        }

        final NodeList list = node.getChildNodes();
        for (int i = 0;
                i < list.getLength();
                ++i) {
            removeNameSpace(doc, list.item(i), ns);
        }
    }

    /**
     * Converts the given TLD using the given stylesheet.
     *
     * @param doc        the given TLD
     * @param stylesheet the given stylesheet
     *
     * @return the converted TLD
     *
     * @throws TransformerFactoryConfigurationError Thrown in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws TransformerConfigurationException    Thrown if there are errors when parsing the
     *                                              {@code Source} or it is not possible to create a
     *                                              {@code Transformer} instance.
     * @throws FactoryConfigurationError            in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws ParserConfigurationException         if a DocumentBuilder cannot be created which
     *                                              satisfies the configuration requested.
     * @throws TransformerException                 If an unrecoverable error occurs during the
     *                                              course of the transformation.
     */
    private Document convertTld(Document doc, String stylesheet) throws
            TransformerFactoryConfigurationError, TransformerConfigurationException,
            FactoryConfigurationError, ParserConfigurationException, TransformerException {

        InputStream xsl = getResourceAsStream(stylesheet);
        Transformer transformer = TransformerFactory.newInstance().newTransformer(
                new StreamSource(xsl));
        Document result = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        transformer.transform(new DOMSource(doc), new DOMResult(result));

        if (isVerbose()) {
            StringWriter sw = new StringWriter();
            sw
                    .append(stylesheet).append(":\n")
                    .append("-".repeat(stylesheet.length() + 1)).append('\n');
            transformer.transform(new DOMSource(doc), new StreamResult(sw));
            println(sw.toString());
        }

        return result;
    }

    /**
     * Populates the root element with any additional information needed before adding this to our
     * tree.
     *
     * @param tagLibrary The tag library being populated
     * @param doc        The TLD DOM to populate.
     */
    private void populateTld(TagLibrary tagLibrary, Document doc) {
        Element root = doc.getDocumentElement();

        checkOrAddShortName(tagLibrary, doc, root);
        checkOrAddAttributeType(doc, root);
        populateTagFileDetails(tagLibrary, doc, root);
    }

    /**
     * Find all tag-file elements and populate them with the actual parsed meta information, found
     * in the tag file's attributes.
     *
     * @param tagLibrary The tag library being populated
     * @param doc        The document we're populating
     * @param root       The root element of the TLD DOM being populated.
     */
    private void populateTagFileDetails(TagLibrary tagLibrary, Document doc, Element root) {
        NodeList tagFileNodes = root.getElementsByTagNameNS("*", "tag-file");
        for (int i = 0; i < tagFileNodes.getLength(); i++) {
            Element tagFileNode = (Element) tagFileNodes.item(i);
            String path = findElementValue(tagFileNode, "path");
            if (path == null) {
                println("WARNING: "
                        + tagLibrary.getPathDescription()
                        + " contains a tag-file element with no path.  Skipping.");
            } else {
                try (InputStream tagFileIn = tagLibrary.getResource(path)) {
                    if (tagFileIn == null) {
                        println("WARNING: Could not find tag file '"
                                + path + "' for tag library "
                                + tagLibrary.getPathDescription()
                                + ".  Data will be incomplete for this tag.");
                    } else {
                        println("Parsing tag file: " + path);
                        TagFile tagFile = TagFile.parse(tagFileIn);
                        for (Directive directive : tagFile.getDirectives()) {
                            String name = directive.getDirectiveName();
                            switch (name) {
                                case "tag":
                                    populateTagFileDetailsTagDirective(
                                            tagFileNode, doc, directive);
                                    break;
                                case "attribute":
                                    populateTagFileDetailsAttributeDirective(
                                            tagFileNode, doc, directive);
                                    break;
                                case "variable":
                                    populateTagFileDetailsVariableDirective(
                                            tagFileNode, doc, directive);
                                    break;
                                default:
                                    break;
                            }
                        }

                        populateTagFileDetailsTagDefaults(tagFileNode, doc,
                                path);
                    }
                } catch (IOException e) {
                    println("WARNING: Could not read tag file '"
                            + path + "' for tag library "
                            + tagLibrary.getPathDescription()
                            + ".  Data will be incomplete for this tag."
                            + "  Reason: " + e.getMessage());
                } catch (ParseException e) {
                    println("WARNING: Could not parse tag file '"
                            + path + "' for tag library "
                            + tagLibrary.getPathDescription()
                            + ".  Data will be incomplete for this tag."
                            + "  Reason: " + e.getMessage());
                }
            }
        }
    }

    /**
     * Populates the given tag-file node with information from the given tag directive.
     *
     * @param tagFileNode The tag-file element
     * @param doc         The document we're populating
     * @param directive   The tag directive to process.
     */
    private void populateTagFileDetailsTagDirective(Element tagFileNode, Document doc,
            Directive directive) {

        for (Attribute attribute : directive.getAttributes()) {
            String name = attribute.getName();
            String value = attribute.getValue();
            Element element;
            if (name.equals("display-name")
                    || name.equals("body-content")
                    || name.equals("dynamic-attributes")
                    || name.equals("description")
                    || name.equals("example")) {
                element = doc.createElementNS(Constants.NS_JAKARTAEE, name);
                element.appendChild(doc.createTextNode(value));
                tagFileNode.appendChild(element);
            } else if (name.equals("small-icon")
                    || name.equals("large-icon")) {
                NodeList icons = tagFileNode.getElementsByTagNameNS("*", "icon");
                Element icon;
                if (icons.getLength() == 0) {
                    icon = doc.createElementNS(Constants.NS_JAKARTAEE, "icon");
                    tagFileNode.appendChild(icon);
                } else {
                    icon = (Element) icons.item(0);
                }
                element = doc.createElementNS(Constants.NS_JAKARTAEE, name);
                element.appendChild(doc.createTextNode(value));
                icon.appendChild(element);
            }
        }
    }

    /**
     * Populates the given tag-file node with default values, for those values not specified via tag
     * directives.
     *
     * @param tagFileNode The tag-file element
     * @param doc         The document we're populating
     * @param path        The path to the tag file
     */
    private void populateTagFileDetailsTagDefaults(Element tagFileNode, Document doc, String path) {
        String displayName = path.substring(
                path.lastIndexOf('/') + 1);
        displayName = displayName.substring(0,
                displayName.lastIndexOf('.'));
        populateDefault(doc, tagFileNode, "display-name", displayName);
        populateDefault(doc, tagFileNode, "body-content", "scriptless");
        populateDefault(doc, tagFileNode, "dynamic-attributes", "false");
    }

    /**
     * Searches for the value of the given element. If no value is found, a default value is
     * inserted.
     *
     * @param doc          The document we're populating
     * @param parent       The element to examine
     * @param tagName      The name of the element we're looking for
     * @param defaultValue The default value to insert, if not found.
     */
    private void populateDefault(Document doc, Element parent, String tagName,
            String defaultValue) {
        if (findElementValue(parent, tagName) == null) {
            Element element = doc.createElementNS(Constants.NS_JAKARTAEE, tagName);
            element.appendChild(doc.createTextNode(defaultValue));
            parent.appendChild(element);
        }
    }

    /**
     * Populates the given tag-file node with information from the given attribute directive.
     *
     * @param tagFileNode The tag-file element
     * @param doc         The document we're populating
     * @param directive   The attribute directive to process.
     */
    private void populateTagFileDetailsAttributeDirective(
            Element tagFileNode, Document doc, Directive directive) {
        Element attributeNode = doc.createElementNS(Constants.NS_JAKARTAEE,
                "attribute");
        tagFileNode.appendChild(attributeNode);
        String deferredValueType = null;
        String deferredMethodSignature = null;
        for (Attribute attribute : directive.getAttributes()) {
            String name = attribute.getName();
            String value = attribute.getValue();
            Element element;
            switch (name) {
                case "name":
                case "required":
                case "fragment":
                case "rtexprvalue":
                case "type":
                case "description":
                    element = doc.createElementNS(Constants.NS_JAKARTAEE, name);
                    element.appendChild(doc.createTextNode(value));
                    attributeNode.appendChild(element);
                    break;
                case "deferredValue":
                    if (deferredValueType == null) {
                        deferredValueType = "java.lang.Object";
                    }
                    break;
                case "deferredValueType":
                    deferredValueType = value;
                    break;
                case "deferredMethod":
                    if (deferredMethodSignature == null) {
                        deferredMethodSignature = "void methodname()";
                    }
                    break;
                case "deferredMethodSignature":
                    deferredMethodSignature = value;
                    break;
                default:
                    break;
            }
        }
        if (deferredValueType != null) {
            Element deferredValueElement
                    = doc.createElementNS(Constants.NS_JAKARTAEE, "deferred-value");
            attributeNode.appendChild(deferredValueElement);
            Element typeElement
                    = doc.createElementNS(Constants.NS_JAKARTAEE, "type");
            typeElement.appendChild(doc.createTextNode(deferredValueType));
            deferredValueElement.appendChild(typeElement);
        }
        if (deferredMethodSignature != null) {
            Element deferredMethodElement
                    = doc.createElementNS(Constants.NS_JAKARTAEE, "deferred-method");
            attributeNode.appendChild(deferredMethodElement);
            Element methodSignatureElement
                    = doc.createElementNS(Constants.NS_JAKARTAEE, "method-signature");
            methodSignatureElement.appendChild(
                    doc.createTextNode(deferredMethodSignature));
            deferredMethodElement.appendChild(methodSignatureElement);
        }
        populateDefault(doc, attributeNode, "required", "false");
        populateDefault(doc, attributeNode, "fragment", "false");
        populateDefault(doc, attributeNode, "rtexprvalue", "false");

        // Default is String if this is not a fragment attribute, or
        // javax.servlet.jsp.tagext.JspFragment if this is a fragment
        // attribute.
        String fragmentValue = findElementValue(attributeNode, "fragment");
        boolean fragment = !(fragmentValue == null
                || fragmentValue.equalsIgnoreCase("false"));
        populateDefault(doc, attributeNode, "type",
                fragment ? "javax.servlet.jsp.tagext.JspFragment"
                        : "java.lang.String");
    }

    /**
     * Populates the given tag-file node with information from the given variable directive.
     *
     * @param tagFileNode The tag-file element
     * @param doc         The document we're populating
     * @param directive   The variable directive to process.
     */
    private void populateTagFileDetailsVariableDirective(
            Element tagFileNode, Document doc, Directive directive) {
        Element variableNode = doc.createElementNS(Constants.NS_JAKARTAEE,
                "variable");
        tagFileNode.appendChild(variableNode);
        for (Attribute attribute : directive.getAttributes()) {
            String name = attribute.getName();
            String value = attribute.getValue();
            Element element;
            if (name.equals("name-given")
                    || name.equals("name-from-attribute")
                    || name.equals("variable-class")
                    || name.equals("declare")
                    || name.equals("scope")
                    || name.equals("description")) {
                element = doc.createElementNS(Constants.NS_JAKARTAEE, name);
                element.appendChild(doc.createTextNode(value));
                variableNode.appendChild(element);
            }
        }
        populateDefault(doc, variableNode, "variable-class",
                "java.lang.String");
        populateDefault(doc, variableNode, "declare", "true");
        populateDefault(doc, variableNode, "scope", "NESTED");
    }

    /**
     * Check to see if there's a short-name element. If not, the TLD is technically invalid, but
     * supply one anyway, and give a warning.
     *
     * @param tagLibrary The tag library being populated
     * @param doc        The TLD DOM to populate.
     * @param root       The root element of the TLD DOM being populated.
     */
    private void checkOrAddShortName(TagLibrary tagLibrary, Document doc,
            Element root) {
        if (root.getElementsByTagNameNS("*", "short-name").getLength() == 0) {
            String prefix = "prefix" + substitutePrefix;
            substitutePrefix++;
            Element shortName = doc.createElementNS(Constants.NS_JAKARTAEE,
                    "short-name");
            shortName.appendChild(doc.createTextNode(prefix));
            root.appendChild(shortName);
            println("WARNING: "
                    + tagLibrary.getPathDescription()
                    + " is missing a short-name element.  Using "
                    + prefix + ".");
        }
    }

    /**
     * Check for empty attribute types and fill in the correct value. This is more difficult for the
     * XSLT transform to do since the default is different depending on whether it is a fragment
     * attribute or not.
     *
     * @param doc  The TLD DOM to populate.
     * @param root The root element of the TLD DOM being populated.
     */
    private void checkOrAddAttributeType(Document doc, Element root) {
        NodeList tagNodes = root.getElementsByTagNameNS("*", "tag");
        for (int i = 0; i < tagNodes.getLength(); i++) {
            Element tagElement = (Element) tagNodes.item(i);
            NodeList attributeNodes
                    = tagElement.getElementsByTagNameNS("*", "attribute");
            for (int j = 0; j < attributeNodes.getLength(); j++) {
                Element attributeElement = (Element) attributeNodes.item(j);
                if (attributeElement.getElementsByTagNameNS("*",
                        "type").getLength() == 0) {
                    // No attribute type specified.
                    String defaultType = "java.lang.String";

                    // Check if there is a fragment element set to true:
                    String fragment = findElementValue(attributeElement,
                            "fragment");
                    if (fragment != null
                            && (fragment.trim().equalsIgnoreCase("true")
                            || fragment.trim().equalsIgnoreCase("yes"))) {
                        defaultType = "javax.servlet.jsp.tagext.JspFragment";
                    }

                    // Create <type> element and append to attribute
                    Element typeElement = doc.createElementNS(
                            Constants.NS_JAKARTAEE, "type");
                    typeElement.appendChild(
                            doc.createTextNode(defaultType));
                    attributeElement.appendChild(typeElement);
                }
            }
        }
    }

    /**
     * Generates all overview files, summarizing all TLDs.
     *
     * @throws TransformerFactoryConfigurationError Thrown in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws TransformerConfigurationException    Thrown if there are errors when parsing the
     *                                              {@code Source} or it is not possible to create a
     *                                              {@code Transformer} instance.
     * @throws TransformerException                 If an unrecoverable error occurs during the
     *                                              course of the transformation.
     */
    private void generateOverview() throws TransformerFactoryConfigurationError,
            TransformerConfigurationException, TransformerException {

        generatePage(outputDirectory.resolve("index.html"), RESOURCE_PATH + "/index.html.xsl");
        generatePage(outputDirectory.resolve("help-doc.html"),
                RESOURCE_PATH + "/help-doc.html.xsl");
        generatePage(outputDirectory.resolve("overview-frame.html"),
                RESOURCE_PATH + "/overview-frame.html.xsl");
        generatePage(outputDirectory.resolve("alltags-frame.html"),
                RESOURCE_PATH + "/alltags-frame.html.xsl");
        generatePage(outputDirectory.resolve("alltags-noframe.html"),
                RESOURCE_PATH + "/alltags-noframe.html.xsl");
        generatePage(outputDirectory.resolve("overview-summary.html"),
                RESOURCE_PATH + "/overview-summary.html.xsl");
    }

    /**
     * Generates all the detail folders for each TLD.
     *
     * @throws IOException          if an I/O error has occurred
     * @throws TransformerException If an unrecoverable error occurs during the course of the
     *                              transformation.
     * @throws GeneratorException   any error during generation
     */
    private void generateTldDetail() throws IOException, TransformerException, GeneratorException {
        ArrayList<String> shortNames = new ArrayList<>();
        Element root = summaryTld.getDocumentElement();
        NodeList taglibs = root.getElementsByTagNameNS("*", "taglib");
        int size = taglibs.getLength();
        for (int i = 0; i < size; i++) {
            Element taglib = (Element) taglibs.item(i);
            String shortName = findElementValue(taglib, "short-name");
            String displayName = findElementValue(taglib, "display-name");
            if (shortNames.contains(shortName)) {
                throw new GeneratorException("Two tag libraries exist with the same short-name '"
                        + shortName + "'.  This is not yet supported.");
            }
            String name = displayName;
            if (name == null) {
                name = shortName;
            }
            println("Generating docs for " + name + "...");
            shortNames.add(shortName);
            Path outDir = outputDirectory.resolve(shortName);
            Files.createDirectories(outDir);

            // Generate information for each TLD:
            generateTldDetail(outDir, shortName);

            // Generate information for each tag:
            NodeList tags = taglib.getElementsByTagNameNS("*", "tag");
            int numTags = tags.getLength();
            for (int j = 0; j < numTags; j++) {
                Element tag = (Element) tags.item(j);
                String tagName = findElementValue(tag, "name");
                generateTagDetail(outDir, shortName, tagName);
            }

            // Generate information for each tag-file:
            NodeList tagFiles = taglib.getElementsByTagNameNS("*", "tag-file");
            int numTagFiles = tagFiles.getLength();
            for (int j = 0; j < numTagFiles; j++) {
                Element tagFile = (Element) tagFiles.item(j);
                String tagFileName = findElementValue(tagFile, "name");
                generateTagDetail(outDir, shortName, tagFileName);
            }

            // Generate information for each function:
            NodeList functions = taglib.getElementsByTagNameNS("*", "function");
            int numFunctions = functions.getLength();
            for (int j = 0; j < numFunctions; j++) {
                Element function = (Element) functions.item(j);
                String functionName = findElementValue(function, "name");
                generateFunctionDetail(outDir, shortName, functionName);
            }
        }
    }

    /**
     * Generates the detail content for the tag library with the given short-name. Files will be
     * placed in outdir.
     *
     * @param outDir    the output directory for generated file
     * @param shortName the short-name of the tag library
     *
     * @throws IOException          if an I/O error has occurred
     * @throws TransformerException If an unrecoverable error occurs during the course of the
     *                              transformation.
     */
    private void generateTldDetail(Path outDir, String shortName) throws IOException,
            TransformerException {

        HashMap<String, String> parameters = new HashMap<>();
        parameters.put("tlddoc-shortName", shortName);

        generatePage(outDir.resolve("tld-frame.html"),
                RESOURCE_PATH + "/tld-frame.html.xsl", parameters);
        generatePage(outDir.resolve("tld-summary.html"),
                RESOURCE_PATH + "/tld-summary.html.xsl", parameters);
    }

    /**
     * Generates the detail content for the tag with the given name in the tag library with the
     * given short-name. Files will be placed in outdir.
     *
     * @param outDir    the output directory for generated file
     * @param shortName the short-name of the tag library
     * @param tagName   the tag-name of the tag library
     *
     * @throws IOException          if an I/O error has occurred
     * @throws TransformerException If an unrecoverable error occurs during the course of the
     *                              transformation.
     */
    private void generateTagDetail(Path outDir, String shortName, String tagName) throws
            IOException, TransformerException {

        HashMap<String, String> parameters = new HashMap<>();
        parameters.put("tlddoc-shortName", shortName);
        parameters.put("tlddoc-tagName", tagName);

        generatePage(outDir.resolve(tagName + ".html"),
                RESOURCE_PATH + "/tag.html.xsl", parameters);
    }

    /**
     * Generates the detail content for the function with the given name in the tag library with the
     * given short-name. Files will be placed in outdir.
     *
     * @param outDir       the output directory for generated file
     * @param shortName    the short-name of the tag library
     * @param functionName the function-name of the tag library
     *
     * @throws IOException          if an I/O error has occurred
     * @throws TransformerException If an unrecoverable error occurs during the course of the
     *                              transformation.
     */
    private void generateFunctionDetail(Path outDir, String shortName, String functionName) throws
            IOException, TransformerException {

        HashMap<String, String> parameters = new HashMap<>();
        parameters.put("tlddoc-shortName", shortName);
        parameters.put("tlddoc-functionName", functionName);

        generatePage(outDir.resolve(functionName + ".fn.html"),
                RESOURCE_PATH + "/function.html.xsl", parameters);
    }

    /**
     * Searches through the given element and returns the value of the body of the element. Returns
     * {@code null} if the element was not found.
     *
     * @param parent  the element to get the value
     * @param tagName the tag-name of the tag library
     *
     * @return the value of the body of the element
     */
    private String findElementValue(Element parent, String tagName) {
        String result = null;
        NodeList elements = parent.getElementsByTagNameNS("*", tagName);
        if (elements.getLength() >= 1) {
            Element child = (Element) elements.item(0);
            Node body = child.getFirstChild();
            if (body.getNodeType() == Node.TEXT_NODE) {
                result = body.getNodeValue();
            }
        }
        return result;
    }

    /**
     * Generates the given page dynamically, by running the summary document through the given XSLT
     * transform. Assumes no parameters.
     *
     * @param outFile  The target file
     * @param inputXsl The stylesheet to use for the transformation
     *
     * @throws TransformerFactoryConfigurationError Thrown in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws TransformerConfigurationException    Thrown if there are errors when parsing the
     *                                              {@code Source} or it is not possible to create a
     *                                              {@code Transformer} instance.
     * @throws TransformerException                 If an unrecoverable error occurs during the
     *                                              course of the transformation.
     */
    private void generatePage(Path outFile, String inputXsl) throws
            TransformerFactoryConfigurationError, TransformerConfigurationException,
            TransformerException {

        generatePage(outFile, inputXsl, null);
    }

    /**
     * Generates the given page dynamically, by running the summary document through the given XSLT
     * transform.
     *
     * @param outFile    The target file
     * @param inputXsl   The stylesheet to use for the transformation
     * @param parameters String key and Object value pairs to pass to the transformation.
     *
     * @throws TransformerFactoryConfigurationError Thrown in case of {@linkplain
     * java.util.ServiceConfigurationError service configuration error} or if the implementation is
     *                                              not available or cannot be instantiated.
     * @throws TransformerConfigurationException    Thrown if there are errors when parsing the
     *                                              {@code Source} or it is not possible to create a
     *                                              {@code Transformer} instance.
     * @throws TransformerException                 If an unrecoverable error occurs during the
     *                                              course of the transformation.
     */
    private void generatePage(Path outFile, String inputXsl, Map<String, String> parameters) throws
            TransformerFactoryConfigurationError, TransformerConfigurationException,
            TransformerException {

        InputStream xsl = getResourceAsStream(inputXsl);
        Transformer transformer = TransformerFactory.newInstance().newTransformer(
                new StreamSource(xsl));
        if (parameters != null) {
            for (Entry<String, String> entry : parameters.entrySet()) {
                transformer.setParameter(entry.getKey(), entry.getValue());
            }
        }
        transformer.transform(new DOMSource(summaryTld), new StreamResult(outFile.toFile()));
    }

    /**
     * Copy the given resource to the given output file. The classloader that loaded TldDocGenerator
     * will be used to find the resource. If xsltDirectory is not null, the files are copied from
     * that directory instead.
     *
     * @param outputFile The destination file
     * @param resource   The resource to copy, starting with '/'
     *
     * @throws IOException if an I/O error has occurred
     */
    private void copyResourceToFile(Path outputFile, String resource) throws IOException {
        try (InputStream in = getResourceAsStream(resource)) {
            Files.copy(in, outputFile, StandardCopyOption.REPLACE_EXISTING);
        }
    }

    /**
     * Outputs the given message to {@code stdout}, only if {@code !quiet}.
     *
     * @param message the message to {@code stdout}
     */
    private void println(String message) {
        if (!quiet) {
            System.out.println(message);
        }
    }

    /**
     * If {@code xsltDirectory} is {@code null}, obtains an {@code InputStream} of the given
     * resource from {@code RESOURCE_PATH}, using the class loader that loaded
     * {@code TldDocGenerator}. Otherwise, finds the file in {@code xsltDirectory} and defaults to
     * the default stylesheet if it has not been overridden.
     *
     * @param resource must start with {@code RESOURCE_PATH}.
     *
     * @return An InputStream for the given resource.
     */
    private InputStream getResourceAsStream(String resource) {
        InputStream result = null;

        if (xsltDirectory != null) {
            Path resourceFile = xsltDirectory.resolve(
                    resource.substring(RESOURCE_PATH.length() + 1));
            try {
                result = Files.newInputStream(resourceFile);
            } catch (IOException e) {
                // result will be null and we'll default to default stylesheet
                println("XSLT-Directory not found, use default-stylesheet: " + e.getMessage());
            }
        }

        if (result == null) {
            result = TldDocGenerator.class.getResourceAsStream(resource);
        }

        return result;
    }

    /**
     * Displays a "success" message.
     */
    private void outputSuccessMessage() {
        println("\nDocumentation generated.");
    }
}