com.servoy.eclipse.docgenerator.DocumentationBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.servoy.eclipse.docgenerator.DocumentationBuilder.java

Source

/*
 This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
    
 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU Affero General Public License as published by the Free
 Software Foundation; either version 3 of the License, or (at your option) any
 later version.
    
 This program is distributed in the hope that it will be useful, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
    
 You should have received a copy of the GNU Affero General Public License along
 with this program; if not, see http://www.gnu.org/licenses or write to the Free
 Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
 */

package com.servoy.eclipse.docgenerator;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;

import com.servoy.eclipse.docgenerator.generators.DefaultDocumentationGenerator;
import com.servoy.eclipse.docgenerator.generators.IDocumentationGenerator;
import com.servoy.eclipse.docgenerator.metamodel.DocumentationWarning;
import com.servoy.eclipse.docgenerator.metamodel.MetaModelHolder;
import com.servoy.eclipse.docgenerator.parser.JavadocExtractor;
import com.servoy.eclipse.docgenerator.parser.ServoyPluginDetector;
import com.servoy.eclipse.docgenerator.parser.SourceCodeTracker;
import com.servoy.eclipse.docgenerator.service.DocumentationGenerationRequest;
import com.servoy.eclipse.docgenerator.service.LogUtil;

/**
 * Builds a documentation XML based on a documentation generation request.
 * 
 * @author gerzse
 */
public class DocumentationBuilder {
    /**
     * The name of the XML file which holds documentation and other Servoy extension related info.
     */
    public static final String EXTENSION_XML_FILE = "servoy-extension.xml";

    /**
     * The documentation generation request.
     */
    private final DocumentationGenerationRequest req;

    /**
     * The parser that is used for parsing Java code.
     */
    private final ASTParser parser;

    public DocumentationBuilder(DocumentationGenerationRequest req) {
        this.req = req;
        parser = ASTParser.newParser(AST.JLS3);
    }

    public void build() {
        Date start = Calendar.getInstance().getTime();
        LogUtil.logger().fine("Documentation build started at " + start.toString() + ".");

        List<IPath> xmlFiles = new ArrayList<IPath>();
        List<IPath> warningsFiles = new ArrayList<IPath>();
        List<Throwable> exceptions = new ArrayList<Throwable>();
        try {
            // If autopilot is on, then scan all packages and find all topmost packages that
            // contain a Servoy plugin (a class that implements IClientPlugin). Then generate
            // separate documentation XMLs for each of these found packages.
            if (req.autopilot()) {
                List<String> extraToProcessProjectNames = new ArrayList<String>();
                List<String> extraToProcessPackageNames = new ArrayList<String>();

                List<String> toProcessProjectNames = new ArrayList<String>();
                List<String> toProcessPackageNames = new ArrayList<String>();
                List<IPath> toProcessXmlFiles = new ArrayList<IPath>();
                LogUtil.logger().fine("Autopilot is on. Scanning for packages that contain Servoy plugins...");
                IWorkspace workspace = ResourcesPlugin.getWorkspace();
                IWorkspaceRoot root = workspace.getRoot();
                LogUtil.logger().fine("Workspace root is '" + root.getLocation().toPortableString() + "'.");
                IProject[] projects = root.getProjects();
                LogUtil.logger().fine("There are " + projects.length + " projects in the workspace.");
                for (int i = 0; i < projects.length && !req.cancelRequested(); i++) {
                    IProject prj = projects[i];
                    if (prj.isOpen() && prj.isNatureEnabled("org.eclipse.jdt.core.javanature")) {
                        if (req.getProjectsAndPackagesToDocument().containsKey(prj.getName())) {
                            Set<String> visitedPackages = new HashSet<String>();
                            List<String> packages = req.getProjectsAndPackagesToDocument().get(prj.getName());
                            IJavaProject jPrj = JavaCore.create(prj);
                            IPackageFragment[] fragments = jPrj.getPackageFragments();
                            for (int j = 0; j < fragments.length && !req.cancelRequested(); j++) {
                                IPackageFragment pkg = fragments[j];
                                if (pkg.getKind() == IPackageFragmentRoot.K_SOURCE) {
                                    String thisPackageName = pkg.getElementName();
                                    // check if the current package is listed among the packages
                                    // to document for the current project (or is a subpackage of
                                    // a listed package)
                                    boolean process = false;
                                    for (String prefix : packages) {
                                        if (matchesPackage(thisPackageName, prefix)) {
                                            process = true;
                                            break;
                                        }
                                    }
                                    if (process) {
                                        // First check if the current package has at least one class that is
                                        // a Servoy plugin.
                                        ServoyPluginDetector pluginDetectorVisitor = new ServoyPluginDetector();
                                        ICompilationUnit[] units = pkg.getCompilationUnits();
                                        for (int k = 0; k < units.length && !req.cancelRequested()
                                                && !pluginDetectorVisitor.containsPlugin(); k++) {
                                            ICompilationUnit comp = units[k];

                                            // We must reconfigure the parser each time, because createAST clears all settings.
                                            parser.setResolveBindings(true);
                                            parser.setIgnoreMethodBodies(true);
                                            parser.setProject(pkg.getJavaProject());
                                            parser.setSource(comp);

                                            // Create the AST and send it to the visitor.
                                            ASTNode cu = parser.createAST(null);
                                            cu.accept(pluginDetectorVisitor);
                                        }
                                        if (!req.cancelRequested()) {
                                            // If the current package should be documented, then schedule it for processing,
                                            // unless it has a parent package that also needs to be processed.
                                            if (pluginDetectorVisitor.containsPlugin()) {
                                                boolean hasVisitedParent = false;
                                                for (String parent : visitedPackages) {
                                                    if (thisPackageName.startsWith(parent + ".")) {
                                                        hasVisitedParent = true;
                                                        break;
                                                    }
                                                }
                                                if (!hasVisitedParent) {
                                                    LogUtil.logger().fine("Will process package '" + thisPackageName
                                                            + "' in project '" + prj.getName()
                                                            + "' because it contains a Servoy plugin and autopilot is on.");
                                                    visitedPackages.add(thisPackageName);
                                                    toProcessProjectNames.add(prj.getName());
                                                    toProcessPackageNames.add(thisPackageName);
                                                    IPath xmlFile = pkg.getResource().getFullPath()
                                                            .append(EXTENSION_XML_FILE);
                                                    toProcessXmlFiles.add(xmlFile);
                                                } else {
                                                    LogUtil.logger().fine("Skipping package '" + thisPackageName
                                                            + "' in project '" + prj.getName()
                                                            + "' because it contains a Servoy plugin, but a parent package also contains a Servoy plugin.");
                                                }
                                            } else {
                                                // when documenting plugins with mobile client support, we take non-plugin packages 
                                                // as well (may contain needed interfaces)
                                                // currently this is hardcoded to take in only servoy_base plugin's project
                                                // comment the check below to allow all projects/packages 
                                                if (thisPackageName.contains("base.plugins")
                                                        && prj.getName().contains("servoy_base")) {
                                                    extraToProcessProjectNames.add(prj.getName());
                                                    extraToProcessPackageNames.add(thisPackageName);
                                                    LogUtil.logger().fine("Will process package '" + thisPackageName
                                                            + "' in project '" + prj.getName()
                                                            + "' needed to document plugins with mobile client support.");
                                                }

                                                else {
                                                    LogUtil.logger().fine("Skipping package '" + thisPackageName
                                                            + "' in project '" + prj.getName()
                                                            + "' because it does not contain a Servoy plugin and autopilot is on.");
                                                }
                                            }

                                        }
                                    } else {
                                        LogUtil.logger().fine("Skipping package '" + pkg.getElementName()
                                                + "' in project '" + prj.getName()
                                                + "' because it was not listed among the packages to document.");
                                    }
                                }
                            }
                        } else {
                            LogUtil.logger().fine("Skipping project '" + prj.getName()
                                    + "' because it is not listed among the projects to document.");
                        }
                    } else {
                        LogUtil.logger()
                                .fine("Skipping roject '" + prj.getName() + "' because it is not a Java project.");
                    }
                }
                req.progressUpdate(0);

                if (toProcessProjectNames.size() > 0) {
                    // Process all scheduled packages.
                    int delta = 100 / toProcessProjectNames.size();
                    for (int i = 0; i < toProcessProjectNames.size() && !req.cancelRequested(); i++) {
                        String projectName = toProcessProjectNames.get(i);
                        String packageName = toProcessPackageNames.get(i);
                        IPath xmlFile = toProcessXmlFiles.get(i);
                        // Prepare a dummy map with one entry (the current project) and one package for
                        // the project.
                        Map<String, List<String>> packagesToVisit = new HashMap<String, List<String>>();
                        List<String> listWithThisPackage = new ArrayList<String>();
                        listWithThisPackage.add(packageName);
                        packagesToVisit.put(projectName, listWithThisPackage);
                        IPath warningsFile = xmlFile.removeFileExtension().addFileExtension("warnings.txt");
                        int startPercent = i * delta;
                        int endPercent = startPercent + delta - 1;

                        // this is to include any extra projects/packages that may be needed when documenting plugins with mobile client support 
                        if (extraToProcessProjectNames.size() > 0) {
                            for (int x = 0; x < extraToProcessProjectNames.size() && !req.cancelRequested(); x++) {
                                projectName = extraToProcessProjectNames.get(x);
                                packageName = extraToProcessPackageNames.get(x);

                                Object packages4project = packagesToVisit.get(projectName);
                                if (packages4project != null)
                                    listWithThisPackage = (ArrayList<String>) packages4project;
                                else
                                    listWithThisPackage = new ArrayList<String>();

                                listWithThisPackage.add(packageName);

                                packagesToVisit.put(projectName, listWithThisPackage);
                            }
                        }

                        try {
                            processRoot(packagesToVisit, xmlFile, warningsFile, startPercent, endPercent, xmlFiles,
                                    warningsFiles);
                        } catch (Exception e) {
                            LogUtil.logger().log(Level.SEVERE, "Exception while generating documentation.", e);
                            exceptions.add(e);
                        }
                    }
                } else {
                    LogUtil.logger().fine("No Servoy plugin found.");
                }
                req.progressUpdate(100);
            }

            // If autopilot is off, then just use the parameters sent in the request.
            else {
                IPath xmlFile = req.getOutputFile();
                IPath warningsFile = xmlFile.removeFileExtension().addFileExtension("warnings.txt");
                try {
                    processRoot(req.getProjectsAndPackagesToDocument(), xmlFile, warningsFile, 0, 100, xmlFiles,
                            warningsFiles);
                } catch (Exception e) {
                    LogUtil.logger().log(Level.SEVERE, "Exception while generating documentation.", e);
                    exceptions.add(e);
                }
            }
        } catch (Throwable e) {
            LogUtil.logger().log(Level.SEVERE, "Exception while generating documentation.", e);
            exceptions.add(e);
        }
        req.requestHandled(xmlFiles, warningsFiles, exceptions, req.cancelRequested());

        Date finalEnd = Calendar.getInstance().getTime();
        LogUtil.logger().fine("Documentation post-processing ended at " + finalEnd.toString() + ".");
    }

    /**
     * Given a Java project and a list of packages inside the project, this method builds a documentation XML for each
     * listed package.
     */
    public void processRoot(Map<String, List<String>> packagesByProject, IPath xmlFile, IPath warningsFile,
            int startPercent, int endPercent, List<IPath> xmlFiles, List<IPath> warningsFiles) throws Exception {
        JavadocExtractor javadocExtractorVisitor = new JavadocExtractor();

        if (LogUtil.logger().isLoggable(Level.FINE)) {
            StringBuffer sb = new StringBuffer();
            sb.append("Processing root with ").append(packagesByProject.size()).append(" projects:\n");
            for (String prjName : packagesByProject.keySet()) {
                List<String> packagesNames = packagesByProject.get(prjName);
                sb.append(prjName).append(":");
                for (String packName : packagesNames)
                    sb.append(" ").append(packName);
                sb.append("\n");
            }
            LogUtil.logger().fine(sb.toString());
        }
        int total = 0;
        IWorkspace workspace = ResourcesPlugin.getWorkspace();
        IWorkspaceRoot root = workspace.getRoot();
        LogUtil.logger().fine("Workspace root is '" + root.getLocation().toPortableString() + "'.");
        IProject[] projects = root.getProjects();
        LogUtil.logger().fine("There are " + projects.length + " projects in the workspace.");
        LogUtil.logger().fine("Counting the total number of compilation units.");
        List<IPackageFragment> selectedPackages = new ArrayList<IPackageFragment>();
        for (int i = 0; i < projects.length && !req.cancelRequested(); i++) {
            IProject prj = projects[i];
            if (prj.isOpen() && prj.isNatureEnabled("org.eclipse.jdt.core.javanature")) {
                if (packagesByProject.containsKey(prj.getName())) {
                    List<String> packages = packagesByProject.get(prj.getName());
                    IJavaProject jPrj = JavaCore.create(prj);
                    int prjFiles = 0;
                    IPackageFragment[] fragments = jPrj.getPackageFragments();
                    for (int j = 0; j < fragments.length && !req.cancelRequested(); j++) {
                        IPackageFragment pkg = fragments[j];
                        if (pkg.getKind() == IPackageFragmentRoot.K_SOURCE) {
                            String thisPackageName = pkg.getElementName();
                            String usedPrefix = null;
                            // Check if the package is listed, or if it is a subpackage of a listed package.
                            boolean process = false;
                            for (String prefix : packages) {

                                if (matchesPackage(thisPackageName, prefix)) {
                                    process = true;
                                    usedPrefix = prefix;
                                    break;
                                }
                            }
                            if (process) {
                                // Select this package for later processing.
                                int pkgFiles = pkg.getCompilationUnits().length;
                                selectedPackages.add(pkg);
                                prjFiles += pkgFiles;
                                LogUtil.logger()
                                        .fine("Package '" + pkg.getElementName() + "' in project '" + prj.getName()
                                                + "' will be documented with " + pkgFiles
                                                + " compilation units. The prefix used was '" + usedPrefix + "'.");
                            } else {
                                LogUtil.logger()
                                        .fine("Skipping package '" + pkg.getElementName() + "' in project '"
                                                + prj.getName()
                                                + "' because it was not listed among the packages to document.");
                            }
                        }
                    }
                    LogUtil.logger().fine("Project '" + prj.getName() + "' will be documented with " + prjFiles
                            + " compilation units.");
                    total += prjFiles;
                } else {
                    LogUtil.logger().fine("Skipping project '" + prj.getName()
                            + "' because it is not listed among the projects to document.");
                }
            } else {
                LogUtil.logger().fine("Skipping roject '" + prj.getName() + "' because it is not a Java project.");
            }
        }
        LogUtil.logger().fine("Total number of compilation units to parse is " + total + ".");

        int lastPercent = startPercent;
        int reserve = 2; // 2% for writing the XML
        int visited = 0;
        req.progressUpdate(startPercent);

        // Parse each compilation unit from the selected packages.
        for (int i = 0; i < selectedPackages.size() && !req.cancelRequested(); i++) {
            IPackageFragment pkg = selectedPackages.get(i);
            LogUtil.logger().fine("Parsing compilation units from package '" + pkg.getElementName() + "'.");
            for (int k = 0; k < pkg.getCompilationUnits().length && !req.cancelRequested(); k++) {
                ICompilationUnit comp = pkg.getCompilationUnits()[k];

                // We must reconfigure the parser each time, because createAST clears all settings.
                parser.setResolveBindings(true);
                parser.setIgnoreMethodBodies(true);
                parser.setProject(pkg.getJavaProject());
                parser.setSource(comp);

                String allCode = comp.getSource();
                if (allCode != null) {
                    SourceCodeTracker tracker = new SourceCodeTracker(allCode);
                    javadocExtractorVisitor.setSourceCodeTracker(tracker);
                } else {
                    javadocExtractorVisitor.setSourceCodeTracker(null);
                }

                // Create the AST and send it to the visitor.
                ASTNode cu = parser.createAST(null);
                cu.accept(javadocExtractorVisitor);

                // Report progress. Make sure we don't output the same percentage more than once.
                visited += 1;
                int newPercent = startPercent + (endPercent - startPercent - reserve) * visited / total;
                if (newPercent > lastPercent) {
                    req.progressUpdate(newPercent);
                    lastPercent = newPercent;
                }
            }
        }

        if (!req.cancelRequested()) {
            // Generate the documentation XML and the warnings file.
            MetaModelHolder holder = javadocExtractorVisitor.getRawDataHolder();

            Set<DocumentationWarning> allWarnings = new TreeSet<DocumentationWarning>();

            IDocumentationGenerator docgen;
            if (req.getDocumentationGeneratorID() != null) {
                docgen = Activator.getDefault().getGenerator(req.getDocumentationGeneratorID());
                if (docgen == null) {
                    LogUtil.logger().severe(
                            "Cannot find doc generator with ID '" + req.getDocumentationGeneratorID() + "'.");
                } else {
                    LogUtil.logger()
                            .info("Using doc generator with ID '" + req.getDocumentationGeneratorID() + "'.");
                }
            } else {
                docgen = new DefaultDocumentationGenerator();
                LogUtil.logger().info("Using default doc generator.");
            }

            if (docgen != null) {
                InputStream xmlStream = docgen.generate(req, holder, allWarnings, xmlFile);
                if (xmlStream != null) {
                    if (writeFile(root, xmlFile, xmlStream)) {
                        xmlFiles.add(xmlFile);
                    }
                }
                req.progressUpdate(endPercent - 1);

                if (!req.cancelRequested()) {
                    // If there are warnings, report them.
                    if (allWarnings.size() > 0) {
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        PrintWriter wout = new PrintWriter(baos);
                        wout.println(allWarnings.size() + " warnings");
                        wout.println();
                        for (DocumentationWarning dw : allWarnings) {
                            wout.println(dw.toString());
                        }
                        wout.close();
                        baos.close();
                        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
                        if (writeFile(root, warningsFile, bais)) {
                            warningsFiles.add(warningsFile);
                        }
                        LogUtil.logger().fine("Warnings written to file '" + warningsFile.toOSString() + "'.");
                    }
                    // If there is no warning, empty the warnings file if it already exists,
                    // otherwise don't create it.
                    else {
                        IFile f = root.getFile(warningsFile);
                        if (f.exists()) {
                            f.setContents((InputStream) null, true, true, null);
                        }
                    }
                    req.progressUpdate(endPercent);
                }
            }
        }
    }

    /**
     * @param thisPackageName
     * @param prefix
     * @return
     */
    private boolean matchesPackage(String packageName, String prefix) {
        return packageName.equals(prefix) || packageName.startsWith(prefix + '.');
    }

    private boolean writeFile(IWorkspaceRoot root, IPath path, InputStream content) throws CoreException {
        IFile f = root.getFile(path);
        if (f.exists()) {
            if (!req.confirmResourceOverwrite(path)) {
                return false;
            } else {
                f.setContents(content, true, true, null);
            }
        } else {
            f.create(content, true, null);
        }
        return true;
    }
}