io.siddhi.doc.gen.core.utils.DocumentationUtils.java Source code

Java tutorial

Introduction

Here is the source code for io.siddhi.doc.gen.core.utils.DocumentationUtils.java

Source

/*
 * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.siddhi.doc.gen.core.utils;

import com.google.common.io.Files;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import io.siddhi.annotation.Example;
import io.siddhi.annotation.Extension;
import io.siddhi.annotation.Parameter;
import io.siddhi.annotation.ReturnAttribute;
import io.siddhi.annotation.SystemParameter;
import io.siddhi.doc.gen.core.freemarker.FormatDescriptionMethod;
import io.siddhi.doc.gen.extensions.ExtensionDocCache;
import io.siddhi.doc.gen.extensions.ExtensionDocRetriever;
import io.siddhi.doc.gen.metadata.ExampleMetaData;
import io.siddhi.doc.gen.metadata.ExtensionMetaData;
import io.siddhi.doc.gen.metadata.ExtensionType;
import io.siddhi.doc.gen.metadata.NamespaceMetaData;
import io.siddhi.doc.gen.metadata.ParameterMetaData;
import io.siddhi.doc.gen.metadata.ReturnAttributeMetaData;
import io.siddhi.doc.gen.metadata.SystemParameterMetaData;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

/**
 * Utility class for getting the meta data for the extension processors in Siddhi
 */
public class DocumentationUtils {
    private DocumentationUtils() { // To prevent instantiating utils class
    }

    /**
     * Returns the extension extension meta data
     * Gets the meta data from the siddhi manager
     *
     * @param targetDirectoryPath The path of the target directory of the maven module containing extensions
     * @param logger              The maven plugin logger
     * @return NamespaceMetaData namespace meta data list
     * @throws MojoFailureException   If this fails to access project dependencies
     * @throws MojoExecutionException If the classes directory from which classes are loaded is invalid
     */
    public static List<NamespaceMetaData> getExtensionMetaData(String targetDirectoryPath,
            List<String> runtimeClasspathElements, Log logger) throws MojoFailureException, MojoExecutionException {
        List<NamespaceMetaData> namespaceMetaDataList = new ArrayList<NamespaceMetaData>();
        int urlCount = runtimeClasspathElements.size() + 1; // +1 to include the module's target/classes folder

        // Creating a list of URLs with all project dependencies
        URL[] urls = new URL[urlCount];
        for (int i = 0; i < runtimeClasspathElements.size(); i++) {
            try {
                urls[i] = new File(runtimeClasspathElements.get(i)).toURI().toURL();
            } catch (MalformedURLException e) {
                throw new MojoFailureException(
                        "Unable to access project dependency: " + runtimeClasspathElements.get(i), e);
            }
        }

        File classesDirectory = new File(targetDirectoryPath + File.separator + Constants.CLASSES_DIRECTORY);
        try {
            // Adding the generated classes to the class loader
            urls[urlCount - 1] = classesDirectory.toURI().toURL();
            ClassLoader urlClassLoader = AccessController
                    .doPrivileged((PrivilegedAction<ClassLoader>) () -> new URLClassLoader(urls,
                            Thread.currentThread().getContextClassLoader()));
            // Getting extensions from all the class files in the classes directory
            addExtensionInDirectory(classesDirectory, classesDirectory.getAbsolutePath(), urlClassLoader,
                    namespaceMetaDataList, logger);
        } catch (MalformedURLException e) {
            throw new MojoExecutionException("Invalid classes directory: " + classesDirectory.getAbsolutePath(), e);
        }
        for (NamespaceMetaData aNamespaceMetaData : namespaceMetaDataList) {
            for (List<ExtensionMetaData> extensionMetaData : aNamespaceMetaData.getExtensionMap().values()) {
                Collections.sort(extensionMetaData);
            }
        }
        Collections.sort(namespaceMetaDataList);
        return namespaceMetaDataList;
    }

    /**
     * Generate documentation related files using metadata
     *
     * @param namespaceMetaDataList      Metadata in this repository
     * @param documentationBaseDirectory The path of the directory in which the documentation will be generated
     * @param documentationVersion       The version of the documentation being generated
     * @param logger                     The logger to log errors
     * @throws MojoFailureException if the Mojo fails to find template file or create new documentation file
     */
    public static void generateDocumentation(List<NamespaceMetaData> namespaceMetaDataList,
            String documentationBaseDirectory, String documentationVersion, Log logger)
            throws MojoFailureException {
        // Generating data model
        Map<String, Object> rootDataModel = new HashMap<>();
        rootDataModel.put("metaData", namespaceMetaDataList);
        rootDataModel.put("formatDescription", new FormatDescriptionMethod());
        rootDataModel.put("latestDocumentationVersion", documentationVersion);

        String outputFileRelativePath = Constants.API_SUB_DIRECTORY + File.separator + documentationVersion
                + Constants.MARKDOWN_FILE_EXTENSION;

        generateFileFromTemplate(
                Constants.MARKDOWN_DOCUMENTATION_TEMPLATE + Constants.MARKDOWN_FILE_EXTENSION
                        + Constants.FREEMARKER_TEMPLATE_FILE_EXTENSION,
                rootDataModel, documentationBaseDirectory, outputFileRelativePath);
        File newVersionFile = new File(documentationBaseDirectory + File.separator + outputFileRelativePath);
        File latestLabelFile = new File(documentationBaseDirectory + File.separator + Constants.API_SUB_DIRECTORY
                + File.separator + Constants.LATEST_FILE_NAME + Constants.MARKDOWN_FILE_EXTENSION);
        try {
            Files.copy(newVersionFile, latestLabelFile);
        } catch (IOException e) {
            logger.warn("Failed to generate latest.md file", e);
        }
    }

    /**
     * Update the documentation home page
     *
     * @param inputFile                  The path to the input file
     * @param outputFile                 The path to the output file
     * @param extensionRepositoryName    The name of  the extension repository
     * @param latestDocumentationVersion The version of the latest documentation generated
     * @param namespaceMetaDataList      Metadata in this repository
     * @throws MojoFailureException if the Mojo fails to find template file or create new documentation file
     */
    public static void updateHeadingsInMarkdownFile(File inputFile, File outputFile, String extensionRepositoryName,
            String latestDocumentationVersion, List<NamespaceMetaData> namespaceMetaDataList)
            throws MojoFailureException {
        // Retrieving the content of the README.md file
        List<String> inputFileLines = new ArrayList<>();
        try {
            inputFileLines = Files.readLines(inputFile, Constants.DEFAULT_CHARSET);
        } catch (IOException ignored) {
        }

        // Generating data model
        Map<String, Object> rootDataModel = new HashMap<>();
        rootDataModel.put("inputFileLines", inputFileLines);
        rootDataModel.put("extensionRepositoryName", extensionRepositoryName);
        rootDataModel.put("latestDocumentationVersion", latestDocumentationVersion);
        rootDataModel.put("metaData", namespaceMetaDataList);
        rootDataModel.put("formatDescription", new FormatDescriptionMethod());

        generateFileFromTemplate(
                Constants.MARKDOWN_HEADINGS_UPDATE_TEMPLATE + Constants.MARKDOWN_FILE_EXTENSION
                        + Constants.FREEMARKER_TEMPLATE_FILE_EXTENSION,
                rootDataModel, outputFile.getParent(), outputFile.getName());
    }

    /**
     * Remove the snapshot version documentation files from docs/api directory
     *
     * @param mkdocsConfigFile           The mkdocs configuration file
     * @param documentationBaseDirectory The path of the base directory in which the documentation will be generated
     * @param logger                     The maven plugin logger
     */
    public static void removeSnapshotAPIDocs(File mkdocsConfigFile, String documentationBaseDirectory, Log logger) {
        // Retrieving the documentation file names
        File apiDocsDirectory = new File(documentationBaseDirectory + File.separator + Constants.API_SUB_DIRECTORY);
        String[] documentationFileNames = apiDocsDirectory
                .list((directory, fileName) -> fileName.endsWith(Constants.MARKDOWN_FILE_EXTENSION));

        if (documentationFileNames != null) {
            // Removing snapshot files and creating a list of the files that are left out
            for (String documentationFileName : documentationFileNames) {
                if (documentationFileName
                        .endsWith(Constants.SNAPSHOT_VERSION_POSTFIX + Constants.MARKDOWN_FILE_EXTENSION)) {
                    // Removing the snapshot documentation file
                    File documentationFile = new File(
                            apiDocsDirectory.getAbsolutePath() + File.separator + documentationFileName);
                    if (!documentationFile.delete()) {
                        logger.warn("Failed to delete SNAPSHOT documentation file "
                                + documentationFile.getAbsolutePath());
                    }
                }
            }

        }
    }

    /**
     * This add a new page to the list of pages in the mkdocs configuration
     *
     * @param mkdocsConfigFile           The mkdocs configuration file
     * @param documentationBaseDirectory The base directory of the documentation
     * @throws FileNotFoundException If mkdocs configuration file is not found
     */
    public static void updateAPIPagesInMkdocsConfig(File mkdocsConfigFile, String documentationBaseDirectory)
            throws FileNotFoundException {
        // Retrieving the documentation file names
        File documentationDirectory = new File(
                documentationBaseDirectory + File.separator + Constants.API_SUB_DIRECTORY);
        String[] documentationFiles = documentationDirectory
                .list((directory, fileName) -> fileName.endsWith(Constants.MARKDOWN_FILE_EXTENSION));

        List<String> apiDirectoryContent;
        if (documentationFiles == null) {
            apiDirectoryContent = new ArrayList<>();
        } else {
            apiDirectoryContent = Arrays.asList(documentationFiles);
            apiDirectoryContent.sort(new Comparator<String>() {
                @Override
                public int compare(String s1, String s2) {
                    String[] s1s = s1.split("\\D+");
                    String[] s2s = s2.split("\\D+");
                    int i = 0;
                    while (s1s.length > i || s2s.length > i) {
                        String s1a = "0";
                        String s2a = "0";
                        if (s1s.length > i) {
                            s1a = s1s[i];
                        }
                        if (s2s.length > i) {
                            s2a = s2s[i];
                        }
                        int s1aInt = Integer.parseInt(s1a);
                        int s2aInt = Integer.parseInt(s2a);
                        if (s2aInt > s1aInt) {
                            return 1;
                        } else if (s2aInt < s1aInt) {
                            return -1;
                        }
                        i++;
                    }
                    return 0;
                }
            });
        }

        String latestVersionFile = null;
        if (apiDirectoryContent.size() > 1) {
            String first = apiDirectoryContent.get(0);
            String second = apiDirectoryContent.get(1);
            if (first.equals(Constants.LATEST_FILE_NAME + Constants.MARKDOWN_FILE_EXTENSION)) {
                latestVersionFile = second;
            }
        }

        // Creating yaml parser
        DumperOptions dumperOptions = new DumperOptions();
        dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        Yaml yaml = new Yaml(dumperOptions);

        // Reading the mkdocs configuration
        Map<String, Object> yamlConfig = (Map<String, Object>) yaml
                .load(new InputStreamReader(new FileInputStream(mkdocsConfigFile), Constants.DEFAULT_CHARSET));

        // Getting the pages list
        List<Map<String, Object>> yamlConfigPagesList = (List<Map<String, Object>>) yamlConfig
                .get(Constants.MKDOCS_CONFIG_PAGES_KEY);

        // Creating the new api pages list
        List<Map<String, Object>> apiPagesList = new ArrayList<>();
        for (String apiFile : apiDirectoryContent) {
            String pageName = apiFile.substring(0, apiFile.length() - Constants.MARKDOWN_FILE_EXTENSION.length());

            Map<String, Object> newPage = new HashMap<>();
            if (latestVersionFile != null && pageName.equals(Constants.LATEST_FILE_NAME)) {
                pageName = "Latest (" + latestVersionFile.substring(0,
                        latestVersionFile.length() - Constants.MARKDOWN_FILE_EXTENSION.length()) + ")";
            }
            newPage.put(pageName, Constants.API_SUB_DIRECTORY + Constants.MKDOCS_FILE_SEPARATOR + apiFile);
            apiPagesList.add(newPage);
        }

        // Setting the new api pages
        Map<String, Object> yamlConfigAPIPage = null;
        for (Map<String, Object> yamlConfigPage : yamlConfigPagesList) {
            if (yamlConfigPage.get(Constants.MKDOCS_CONFIG_PAGES_API_KEY) != null) {
                yamlConfigAPIPage = yamlConfigPage;
                break;
            }
        }
        if (yamlConfigAPIPage == null) {
            yamlConfigAPIPage = new HashMap<>();
            yamlConfigPagesList.add(yamlConfigAPIPage);
        }
        yamlConfigAPIPage.put(Constants.MKDOCS_CONFIG_PAGES_API_KEY, apiPagesList);

        // Saving the updated configuration
        yaml.dump(yamlConfig,
                new OutputStreamWriter(new FileOutputStream(mkdocsConfigFile), Constants.DEFAULT_CHARSET));
    }

    /**
     * Generate a extension index file from the template file
     *
     * @param extensionRepositories      The list of extension repository names
     * @param extensionRepositoryOwner   The extension repository owner's name
     * @param documentationBaseDirectory The output directory path in which the extension index will be generated
     * @param extensionsIndexFileName    The name of the index file that will be generated
     * @throws MojoFailureException if the Mojo fails to find template file or create new documentation file
     */
    public static void createExtensionsIndex(List<String> extensionRepositories, String extensionRepositoryOwner,
            String documentationBaseDirectory, String projectBaseDir, String extensionsIndexFileName)
            throws MojoFailureException {
        // Separating Apache and GPL extensions based on siddhi repository prefix conventions
        List<String> gplExtensionRepositories = new ArrayList<>();
        List<String> apacheExtensionRepositories = new ArrayList<>();
        for (String extensionRepository : extensionRepositories) {
            if (extensionRepository.startsWith(Constants.GITHUB_GPL_EXTENSION_REPOSITORY_PREFIX)) {
                gplExtensionRepositories.add(extensionRepository);
            } else if (extensionRepository.startsWith(Constants.GITHUB_APACHE_EXTENSION_REPOSITORY_PREFIX)) {
                apacheExtensionRepositories.add(extensionRepository);
            }
        }

        // Generating data model
        Map<String, Object> rootDataModel = new HashMap<>();
        rootDataModel.put("extensionsOwner", extensionRepositoryOwner);

        Path gplDocCachePath = Paths.get(projectBaseDir, "src", "main", "resources", "gpl.docs.json");
        Path apacheDocCachePath = Paths.get(projectBaseDir, "src", "main", "resources", "apache.docs.json");

        rootDataModel.put("gplExtensions", retrieveExtensionWithDescriptions(gplDocCachePath,
                extensionRepositoryOwner, gplExtensionRepositories));
        rootDataModel.put("apacheExtensions", retrieveExtensionWithDescriptions(apacheDocCachePath,
                extensionRepositoryOwner, apacheExtensionRepositories));

        generateFileFromTemplate(
                Constants.MARKDOWN_EXTENSIONS_INDEX_TEMPLATE + Constants.MARKDOWN_FILE_EXTENSION
                        + Constants.FREEMARKER_TEMPLATE_FILE_EXTENSION,
                rootDataModel, documentationBaseDirectory,
                extensionsIndexFileName + Constants.MARKDOWN_FILE_EXTENSION);
    }

    private static Map<String, String> retrieveExtensionWithDescriptions(Path cachePath,
            String extensionRepositoryOwner, List<String> extensions) throws MojoFailureException {
        ExtensionDocCache cache = new ExtensionDocCache(cachePath);
        ExtensionDocRetriever retriever = new ExtensionDocRetriever(extensionRepositoryOwner, extensions, cache);

        retriever.pull();

        boolean inMemory = cache.isInMemory();
        boolean throttled = retriever.isThrottled();

        if (throttled && inMemory) {
            throw new MojoFailureException(
                    "The API has reached the throttling limits while fetching the extensions."
                            + "The extension cache is also not available."
                            + "Try again later or check whether cache is present in path: " + cachePath.toString());
        }
        return cache.getExtensionDescriptionMap();
    }

    /**
     * Build the mkdocs site using the mkdocs config file
     *
     * @param mkdocsConfigFile The mkdocs configuration file
     * @param logger           The maven logger
     * @return true if the documentation generation is successful
     */
    public static boolean generateMkdocsSite(File mkdocsConfigFile, Log logger) {
        boolean isDocumentationGenerationSuccessful = false;
        try {
            // Building the mkdocs site
            executeCommand(new String[] { Constants.MKDOCS_COMMAND, Constants.MKDOCS_BUILD_COMMAND,
                    Constants.MKDOCS_BUILD_COMMAND_CLEAN_ARGUEMENT,
                    Constants.MKDOCS_BUILD_COMMAND_CONFIG_FILE_ARGUMENT, mkdocsConfigFile.getAbsolutePath(),
                    Constants.MKDOCS_BUILD_COMMAND_SITE_DIRECTORY_ARGUMENT, Constants.MKDOCS_SITE_DIRECTORY },
                    logger);
            isDocumentationGenerationSuccessful = true;
        } catch (Throwable t) {
            logger.warn("Failed to generate the mkdocs site.", t);
        }
        return isDocumentationGenerationSuccessful;
    }

    /**
     * Deploy the mkdocs website on GitHub pages
     *
     * @param version       The version of the documentation
     * @param baseDirectory The base directory of the project
     * @param url           The SCM URL
     * @param scmUsername   The SCM username
     * @param scmPassword   The SCM password
     * @param logger        The maven logger
     */
    public static void deployMkdocsOnGitHubPages(String version, File baseDirectory, String url, String scmUsername,
            String scmPassword, Log logger) {
        try {
            // Find initial branch name
            List<String> gitStatusOutput = getCommandOutput(
                    new String[] { Constants.GIT_COMMAND, Constants.GIT_BRANCH_COMMAND }, logger);
            String initialBranch = null;
            for (String gitStatusOutputLine : gitStatusOutput) {
                if (gitStatusOutputLine.startsWith(Constants.GIT_BRANCH_COMMAND_OUTPUT_CURRENT_BRANCH_PREFIX)) {
                    initialBranch = gitStatusOutputLine
                            .substring(Constants.GIT_BRANCH_COMMAND_OUTPUT_CURRENT_BRANCH_PREFIX.length());
                }
            }

            if (initialBranch != null) {
                // Stash changes
                executeCommand(new String[] { Constants.GIT_COMMAND, Constants.GIT_STASH_COMMAND }, logger);

                // Change to gh-pages branch. This will not do anything if a new branch was created in the last command.
                executeCommand(new String[] { Constants.GIT_COMMAND, Constants.GIT_CHECKOUT_COMMAND,
                        Constants.GIT_GH_PAGES_BRANCH }, logger);

                // Create branch if it does not exist. This will fail if the branch exists and will not do anything.
                executeCommand(
                        new String[] { Constants.GIT_COMMAND, Constants.GIT_CHECKOUT_COMMAND,
                                Constants.GIT_CHECKOUT_COMMAND_ORPHAN_ARGUMENT, Constants.GIT_GH_PAGES_BRANCH },
                        logger);

                executeCommand(new String[] { Constants.GIT_COMMAND, Constants.GIT_PULL_COMMAND,
                        Constants.GIT_REMOTE, Constants.GIT_GH_PAGES_BRANCH }, logger);

                // Getting the site that was built by mkdocs
                File siteDirectory = new File(Constants.MKDOCS_SITE_DIRECTORY);
                FileUtils.copyDirectory(siteDirectory, baseDirectory);
                String[] siteDirectoryContent = siteDirectory.list();

                // Pushing the site to GitHub (Assumes that site/ directory is ignored by git)
                if (siteDirectoryContent != null && siteDirectoryContent.length > 0) {
                    List<String> gitAddCommand = new ArrayList<>();
                    Collections.addAll(gitAddCommand, Constants.GIT_COMMAND, Constants.GIT_ADD_COMMAND);
                    Collections.addAll(gitAddCommand, siteDirectoryContent);
                    executeCommand(gitAddCommand.toArray(new String[gitAddCommand.size()]), logger);

                    List<String> gitCommitCommand = new ArrayList<>();
                    Collections.addAll(gitCommitCommand, Constants.GIT_COMMAND, Constants.GIT_COMMIT_COMMAND,
                            Constants.GIT_COMMIT_COMMAND_MESSAGE_ARGUMENT,
                            String.format(Constants.GIT_COMMIT_COMMAND_MESSAGE_FORMAT, version, version),
                            Constants.GIT_COMMIT_COMMAND_FILES_ARGUMENT);
                    Collections.addAll(gitCommitCommand, siteDirectoryContent);
                    executeCommand(gitCommitCommand.toArray(new String[gitCommitCommand.size()]), logger);

                    if (scmUsername != null && scmPassword != null && url != null) {
                        // Using scm username and password env var values
                        String urlWithUsernameAndPassword = url.replaceFirst(Constants.GITHUB_URL,
                                Constants.GITHUB_URL_WITH_USERNAME_PASSWORD);
                        executeCommand(new String[] { Constants.GIT_COMMAND, Constants.GIT_PUSH_COMMAND,
                                String.format(urlWithUsernameAndPassword, scmUsername, scmPassword),
                                Constants.GIT_GH_PAGES_BRANCH }, logger);

                    } else {
                        // Using git credential store
                        executeCommand(new String[] { Constants.GIT_COMMAND, Constants.GIT_PUSH_COMMAND,
                                Constants.GIT_REMOTE, Constants.GIT_GH_PAGES_BRANCH }, logger);
                    }
                }

                // Changing back to initial branch
                executeCommand(
                        new String[] { Constants.GIT_COMMAND, Constants.GIT_CHECKOUT_COMMAND, initialBranch },
                        logger);
                executeCommand(new String[] { Constants.GIT_COMMAND, Constants.GIT_STASH_COMMAND,
                        Constants.GIT_STASH_POP_COMMAND }, logger);
            } else {
                logger.warn("Unable to parse git-status command and retrieve current git branch. "
                        + "Skipping deployment.");
            }
        } catch (Throwable t) {
            logger.warn("Failed to deploy the documentation on github pages.", t);
        }
    }

    /**
     * Commit the documentation directory and the mkdocs config file
     *
     * @param docsDirectory    The docs drectory
     * @param mkdocsConfigFile The mkdocs configuration file
     * @param readmeFile       The read me file
     * @param version          The version of the documentation
     * @param logger           The maven logger
     */
    public static void updateDocumentationOnGitHub(String docsDirectory, File mkdocsConfigFile, File readmeFile,
            String version, Log logger) {
        try {
            executeCommand(new String[] { Constants.GIT_COMMAND, Constants.GIT_ADD_COMMAND, docsDirectory,
                    mkdocsConfigFile.getAbsolutePath(), readmeFile.getAbsolutePath() }, logger);
            executeCommand(new String[] { Constants.GIT_COMMAND, Constants.GIT_COMMIT_COMMAND,
                    Constants.GIT_COMMIT_COMMAND_MESSAGE_ARGUMENT,
                    String.format(Constants.GIT_COMMIT_COMMAND_MESSAGE_FORMAT, version, version),
                    Constants.GIT_COMMIT_COMMAND_FILES_ARGUMENT, docsDirectory, mkdocsConfigFile.getAbsolutePath(),
                    readmeFile.getAbsolutePath() }, logger);
        } catch (Throwable t) {
            logger.warn("Failed to update the documentation on GitHub repository", t);
        }
    }

    /**
     * Search for class files in the directory and add extensions from them
     * This method recursively searches the sub directories
     *
     * @param moduleTargetClassesDirectory The directory from which the extension metadata will be loaded
     * @param classesDirectoryPath         The absolute path to the classes directory in the target folder in the module
     * @param urlClassLoader               The url class loader which should be used for loading the classes
     * @param namespaceMetaDataList        List of namespace meta data
     * @param logger                       The maven logger
     */
    private static void addExtensionInDirectory(File moduleTargetClassesDirectory, String classesDirectoryPath,
            ClassLoader urlClassLoader, List<NamespaceMetaData> namespaceMetaDataList, Log logger) {
        File[] innerFiles = moduleTargetClassesDirectory.listFiles();
        if (innerFiles != null) {
            for (File innerFile : innerFiles) {
                if (innerFile.isDirectory()) {
                    addExtensionInDirectory(innerFile, classesDirectoryPath, urlClassLoader, namespaceMetaDataList,
                            logger);
                } else {
                    addExtensionInFile(innerFile, classesDirectoryPath, urlClassLoader, namespaceMetaDataList,
                            logger);
                }
            }
        }
    }

    /**
     * Add an extension annotation in a file
     * This first checks if this is a class file and loads the class and adds the annotation if present
     *
     * @param file                  The file from which the extension metadata will be loaded
     * @param classesDirectoryPath  The absolute path to the classes directory in the target folder in the module
     * @param urlClassLoader        The url class loader which should be used for loading the classes
     * @param namespaceMetaDataList List of namespace meta data
     * @param logger                The maven logger
     */
    private static void addExtensionInFile(File file, String classesDirectoryPath, ClassLoader urlClassLoader,
            List<NamespaceMetaData> namespaceMetaDataList, Log logger) {
        String filePath = file.getAbsolutePath();
        if (filePath.endsWith(Constants.CLASS_FILE_EXTENSION)
                && filePath.length() > classesDirectoryPath.length()) {
            String relativePathToClass = filePath.substring((classesDirectoryPath + File.separator).length());

            try {
                // Loading class
                Class<?> extensionClass = Class
                        .forName(
                                relativePathToClass
                                        .substring(0,
                                                relativePathToClass.length()
                                                        - Constants.CLASS_FILE_EXTENSION.length())
                                        .replace(File.separator, "."),
                                false, urlClassLoader);

                // Generating metadata and adding the it to the list of relevant extensions
                addExtensionMetaDataIntoNamespaceList(namespaceMetaDataList, extensionClass, logger);
            } catch (Throwable ignored) {
                logger.warn("Ignoring the failed class loading from " + file.getAbsolutePath());
            }
        }
    }

    /**
     * Generate extension meta data from the annotated data in the class
     *
     * @param namespaceList  The list of namespaces to which the new extension will be added
     * @param extensionClass Class from which meta data should be extracted from
     * @param logger         The maven plugin logger
     */
    private static void addExtensionMetaDataIntoNamespaceList(List<NamespaceMetaData> namespaceList,
            Class<?> extensionClass, Log logger) {
        Extension extensionAnnotation = extensionClass.getAnnotation(Extension.class);

        if (extensionAnnotation != null) { // Discarding extension classes without annotation
            ExtensionMetaData extensionMetaData = new ExtensionMetaData();

            // Finding extension type
            String extensionType = null;
            for (Map.Entry<ExtensionType, Class<?>> entry : ExtensionType.getSuperClassMap().entrySet()) {
                Class<?> superClass = entry.getValue();
                if (superClass.isAssignableFrom(extensionClass) && superClass != extensionClass) {
                    extensionType = entry.getKey().getValue();
                    break;
                }
            }

            // Discarding the extension if it belongs to an unknown type
            if (extensionType == null) {
                logger.warn("Discarding extension (belonging to an unknown extension type): "
                        + extensionClass.getCanonicalName());
                return;
            }

            extensionMetaData.setName(extensionAnnotation.name());
            extensionMetaData.setDescription(extensionAnnotation.description());

            // Adding query parameters
            ParameterMetaData[] parameters = new ParameterMetaData[extensionAnnotation.parameters().length];
            for (int i = 0; i < extensionAnnotation.parameters().length; i++) {
                Parameter parameterAnnotation = extensionAnnotation.parameters()[i];

                ParameterMetaData parameter = new ParameterMetaData();
                parameter.setName(parameterAnnotation.name());
                parameter.setType(Arrays.asList(parameterAnnotation.type()));
                parameter.setDescription(parameterAnnotation.description());
                parameter.setOptional(parameterAnnotation.optional());
                parameter.setDynamic(parameterAnnotation.dynamic());
                parameter.setDefaultValue(parameterAnnotation.defaultValue());
                parameters[i] = parameter;
            }
            extensionMetaData.setParameters(Arrays.asList(parameters));

            // Adding system parameters
            SystemParameterMetaData[] systemParameters = new SystemParameterMetaData[extensionAnnotation
                    .systemParameter().length];
            for (int i = 0; i < extensionAnnotation.systemParameter().length; i++) {
                SystemParameter systemParameterAnnotation = extensionAnnotation.systemParameter()[i];

                SystemParameterMetaData systemParameter = new SystemParameterMetaData();
                systemParameter.setName(systemParameterAnnotation.name());
                systemParameter.setDescription(systemParameterAnnotation.description());
                systemParameter.setDefaultValue(systemParameterAnnotation.defaultValue());
                systemParameter
                        .setPossibleParameters(Arrays.asList(systemParameterAnnotation.possibleParameters()));
                systemParameters[i] = systemParameter;
            }
            extensionMetaData.setSystemParameters(Arrays.asList(systemParameters));

            // Adding return attributes
            ReturnAttributeMetaData[] returnAttributes = new ReturnAttributeMetaData[extensionAnnotation
                    .returnAttributes().length];
            for (int i = 0; i < extensionAnnotation.returnAttributes().length; i++) {
                ReturnAttribute parameterAnnotation = extensionAnnotation.returnAttributes()[i];

                ReturnAttributeMetaData returnAttribute = new ReturnAttributeMetaData();
                returnAttribute.setName(parameterAnnotation.name());
                returnAttribute.setType(Arrays.asList(parameterAnnotation.type()));
                returnAttribute.setDescription(parameterAnnotation.description());
                returnAttributes[i] = returnAttribute;
            }
            extensionMetaData.setReturnAttributes(Arrays.asList(returnAttributes));

            // Adding examples
            ExampleMetaData[] examples = new ExampleMetaData[extensionAnnotation.examples().length];
            for (int i = 0; i < extensionAnnotation.examples().length; i++) {
                Example exampleAnnotation = extensionAnnotation.examples()[i];

                ExampleMetaData exampleMetaData = new ExampleMetaData();
                exampleMetaData.setSyntax(exampleAnnotation.syntax());
                exampleMetaData.setDescription(exampleAnnotation.description());
                examples[i] = exampleMetaData;
            }
            extensionMetaData.setExamples(Arrays.asList(examples));

            // Finding the namespace
            String namespaceName = extensionAnnotation.namespace();
            if (Objects.equals(namespaceName, "")) {
                namespaceName = Constants.CORE_NAMESPACE;
            }

            // Finding the relevant namespace in the namespace list
            NamespaceMetaData namespace = null;
            for (NamespaceMetaData existingNamespace : namespaceList) {
                if (Objects.equals(existingNamespace.getName(), namespaceName)) {
                    namespace = existingNamespace;
                    break;
                }
            }
            // Creating namespace if it doesn't exist
            if (namespace == null) {
                namespace = new NamespaceMetaData();
                namespace.setName(namespaceName);
                namespace.setExtensionMap(new TreeMap<>());
                namespaceList.add(namespace);
            }

            // Adding to the relevant extension metadata list in the namespace
            List<ExtensionMetaData> extensionMetaDataList = namespace.getExtensionMap()
                    .computeIfAbsent(extensionType, k -> new ArrayList<>());

            extensionMetaDataList.add(extensionMetaData);
        }
    }

    /**
     * Generate a file from a template
     *
     * @param templateFile    The template file name
     * @param dataModel       The data model to be used for generating the output files from template files
     * @param outputDirectory The output directory in which the file will be generated
     * @param outputFileName  The name of the file that will be generated
     * @throws MojoFailureException if the Mojo fails to find template file or create new documentation file
     */
    private static void generateFileFromTemplate(String templateFile, Map<String, Object> dataModel,
            String outputDirectory, String outputFileName) throws MojoFailureException {
        // Creating the free marker configuration
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_25);
        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        cfg.setClassForTemplateLoading(DocumentationUtils.class, File.separator + Constants.TEMPLATES_DIRECTORY);

        // Adding the constants to the freemarker data model
        Map<String, String> constantsClassFieldMap = new HashMap<>();
        for (Field field : Constants.class.getDeclaredFields()) {
            try {
                constantsClassFieldMap.put(field.getName(), field.get(null).toString());
            } catch (IllegalAccessException ignored) { // Ignoring inaccessible variables
            }
        }
        dataModel.put("CONSTANTS", constantsClassFieldMap);

        // Adding the ExtensionType enum values to the freemarker data model
        Map<String, String> extensionTypeEnumMap = new HashMap<>();
        for (Field field : ExtensionType.class.getDeclaredFields()) {
            try {
                if (field.isEnumConstant()) {
                    extensionTypeEnumMap.put(field.getName(), ((ExtensionType) field.get(null)).getValue());
                }
            } catch (IllegalAccessException ignored) { // Ignoring inaccessible variables
            }
        }
        dataModel.put("EXTENSION_TYPE", extensionTypeEnumMap);

        try {
            // Fetching the template
            Template template = cfg.getTemplate(templateFile);

            // Generating empty documentation files
            File outputFile = new File(outputDirectory + File.separator + outputFileName);
            if (!outputFile.getParentFile().exists()) {
                if (!outputFile.getParentFile().mkdirs()) {
                    throw new MojoFailureException("Unable to create directory " + outputFile.getParentFile());
                }
            }
            if (!outputFile.exists()) {
                if (!outputFile.createNewFile()) {
                    throw new MojoFailureException("Unable to create file " + outputFile.getAbsolutePath());
                }
            }

            // Writing to the documentation file
            try (OutputStream outputStream = new FileOutputStream(outputFile)) {
                try (Writer writer = new OutputStreamWriter(outputStream, Charset.defaultCharset())) {
                    template.process(dataModel, writer);
                }
            } catch (TemplateException e) {
                throw new MojoFailureException("Invalid Free Marker template found in " + templateFile, e);
            }
        } catch (IOException e) {
            throw new MojoFailureException("Unable to find template file " + templateFile, e);
        }
    }

    /**
     * Executing a command
     *
     * @param command The command to be executed
     * @param logger  The maven plugin logger
     * @return The output lines from executing the command
     * @throws Throwable if any error occurs during the execution of the command
     */
    private static List<String> getCommandOutput(String[] command, Log logger) throws Throwable {
        logger.info("Executing: " + String.join(" ", command));
        Process process = Runtime.getRuntime().exec(command);
        List<String> executionOutputLines = new ArrayList<>();

        // Logging the output of the command execution
        InputStream[] inputStreams = new InputStream[] { process.getInputStream(), process.getErrorStream() };
        BufferedReader bufferedReader = null;
        try {
            for (InputStream inputStream : inputStreams) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Constants.DEFAULT_CHARSET));
                String commandOutput;
                while (true) {
                    commandOutput = bufferedReader.readLine();
                    if (commandOutput == null) {
                        break;
                    }

                    executionOutputLines.add(commandOutput);
                }
            }
            process.waitFor();
        } finally {
            IOUtils.closeQuietly(bufferedReader);
        }

        return executionOutputLines;
    }

    /**
     * Executing a command.
     *
     * @param command The command to be executed
     * @param logger  The maven plugin logger
     * @throws Throwable if any error occurs during the execution of the command
     */
    private static void executeCommand(String[] command, Log logger) throws Throwable {
        List<String> executionOutputLines = getCommandOutput(command, logger);
        for (String executionOutputLine : executionOutputLines) {
            logger.debug(executionOutputLine);
        }
    }
}