All Downloads are FREE. Search and download functionalities are using the official Maven repository.

za.co.mip.ablduck.ABLDuck Maven / Gradle / Ivy

There is a newer version: 229
Show newest version
/**
 * Copyright 2017-2023 MIP Holdings
 *
 *  Licensed 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 za.co.mip.ablduck;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.Format;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.openedge.core.metadata.DataTypes;
import com.openedge.core.metadata.IDataType;
import com.openedge.core.runtime.IPropath;
import com.openedge.core.runtime.Propath;
import com.openedge.pdt.core.ast.ASTManager;
import com.openedge.pdt.core.ast.IASTManager;
import com.openedge.pdt.core.ast.PropathASTContext;
import com.openedge.pdt.core.ast.model.IASTContext;
import com.openedge.pdt.core.ast.model.ICompilationUnit;
import com.phenix.pct.Messages;
import com.phenix.pct.PCT;
import com.phenix.pct.Version;

import za.co.mip.ablduck.models.Cls;
import za.co.mip.ablduck.models.CompilationUnit;
import za.co.mip.ablduck.models.Data;
import za.co.mip.ablduck.models.Member;
import za.co.mip.ablduck.models.Parameter;
import za.co.mip.ablduck.models.Procedure;
import za.co.mip.ablduck.models.Search;

/**
 * Class for generating ABLDuck documentation from OpenEdge classes
 * 
 * @author Robert Edwards
 */
public class ABLDuck extends PCT {
    private static final int BUFFER_SIZE = 4096;
    private static final String ICON_PREFIX = "icon-";

    private Data data = new Data();
    private HashMap classes = new HashMap<>();
    private HashMap procedures = new HashMap<>();
    private String title = "ABLDuck documentation";
    private File destDir = null;
    private File destDirOutput = null;
    private Boolean dataFilesOnly = false;
    private List filesets = new ArrayList<>();
    protected Path propath = null;
    private String encoding = null;
    private Charset inputCharset = null;
    private File template = null;
    private File customLink = null;

    public ABLDuck() {
        super();
        createPropath();
    }

    /**
     * Adds a set of files to archive.
     * 
     * @param set FileSet
     */
    public void addFileset(FileSet set) {
        filesets.add(set);
    }

    /**
     * Destination directory
     */
    public void setDestDir(File dir) {
        this.destDir = dir;
        this.destDirOutput = new File(dir, "output");
    }

    /**
     * Documentation title
     * 
     * @param title Documentation title
     */
    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * Codepage to use when reading files
     * 
     * @param encoding String
     */
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    /**
     * Set the propath to be used when running the procedure
     * 
     * @param propath an Ant Path object containing the propath
     */
    public void addPropath(Path propath) {
        createPropath().append(propath);
    }

    /**
     * Creates a new Path instance
     * 
     * @return Path
     */
    public Path createPropath() {
        if (this.propath == null) {
            this.propath = new Path(this.getProject());
        }

        return this.propath;
    }

    /**
     * Generate the data files only
     * 
     * @param dataOnly Generate only the data files, wont extract the template
     */
    public void setDataFilesOnly(Boolean dataOnly) {
        this.dataFilesOnly = dataOnly;
    }

    /**
     * Set the template directory or zip (if null, we use default template)
     * 
     * @param template directory of template file
     */
    public void setTemplate(File template) {
        this.template = template;
    }

    /**
     * Set a customLink File
     * 
     * @param customLink customLinkFile
     */
    public void setCustomLink(File customLink) {
        this.customLink = customLink;
    }

    @Override
    public void execute() {

        // Destination directory must exist
        if (this.destDir == null) {
            throw new BuildException(MessageFormat
                    .format(Messages.getString("OpenEdgeClassDocumentation.0"), "destDir"));
        }
        // There must be at least one fileset
        if (filesets.isEmpty()) {
            throw new BuildException(Messages.getString("OpenEdgeClassDocumentation.1"));
        }

        this.destDirOutput.mkdirs();

        if (!this.dataFilesOnly) {
            // Extract template
            try {
                if (template != null && this.template.isDirectory()) {
                    copyFullRecursive(this.template, this.destDir);
                } else if (this.template != null && this.template.isFile()) {
                    unzip(new FileInputStream(this.template), this.destDir);
                } else {
                    extractTemplateDirectory(this.destDir);
                }

                // CustomLinkFile
                String vCustomLink = "resources/customlink.js";
                if (this.customLink != null && this.customLink.isFile()) {
                    vCustomLink = this.customLink.getName();
                    copyFullRecursive(this.customLink, new File(this.destDir, vCustomLink));
                }

                Format formatter = new SimpleDateFormat("EEE d MMM yyyy HH:mm:ss");
                List files = Arrays.asList("index.html"); // , "template.html",
                                                                  // "print-template.html"

                for (String file : files) {
                    replaceTemplateTags("{title}", this.title,
                            Paths.get(this.destDir.getAbsolutePath(), file));
                    replaceTemplateTags("{version}", Version.getPCTVersion(),
                            Paths.get(this.destDir.getAbsolutePath(), file));
                    replaceTemplateTags("{date}", formatter.format(new Date()),
                            Paths.get(this.destDir.getAbsolutePath(), file));
                    replaceTemplateTags("{customLinkFile}", vCustomLink,
                            Paths.get(this.destDir.getAbsolutePath(), file));
                }
            } catch (IOException ex) {
                throw new BuildException(ex);
            }
        }

        Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();

        IPropath pp = new Propath(
                new org.eclipse.core.runtime.Path(getProject().getBaseDir().getAbsolutePath()),
                propath.list());
        IASTContext astContext = new PropathASTContext(pp);
        IProgressMonitor monitor = new NullProgressMonitor();
        IASTManager astMgr = ASTManager.getASTManager();

        log("Generating ABLDuck documentation (11.5+ method)", Project.MSG_INFO);

        for (FileSet fs : filesets) {
            // And get files from fileset
            String[] dsfiles = fs.getDirectoryScanner(this.getProject()).getIncludedFiles();

            for (int i = 0; i < dsfiles.length; i++) {
                File file = new File(fs.getDir(this.getProject()), dsfiles[i]);
                log("Generating AST for " + file.getAbsolutePath(), Project.MSG_VERBOSE);

                int extPos = file.getName().lastIndexOf('.');
                String ext = file.getName().substring(extPos);
                boolean isClass = ".cls".equalsIgnoreCase(ext);

                try (InputStream input = new FileInputStream(file);
                        Reader fsr = new InputStreamReader(input, getCharset())) {
                    ICompilationUnit root = astMgr.createAST(fsr, astContext, monitor,
                            IASTManager.EXPAND_ON, IASTManager.DLEVEL_FULL);
                    if (isClass) {
                        ABLDuckClassVisitor visitor = new ABLDuckClassVisitor(pp);
                        log("Executing AST ClassVisitor " + file.getAbsolutePath(),
                                Project.MSG_VERBOSE);
                        root.accept(visitor);

                        CompilationUnit cu = visitor.getCompilationUnit();
                        classes.put(cu.name, cu);
                    } else {

                        ABLDuckProcedureVisitor visitor = new ABLDuckProcedureVisitor(dsfiles[i]);
                        log("Executing AST ProcedureVisitor " + file.getAbsolutePath(),
                                Project.MSG_VERBOSE);
                        root.accept(visitor);

                        CompilationUnit cu = visitor.getCompilationUnit();
                        procedures.put(cu.name, cu);
                    }
                } catch (IOException e) {
                    log(e.getMessage(), Project.MSG_WARN);
                }
            }
        }

        // Procedures search objects
        for (Map.Entry procedureEntry : procedures.entrySet()) {
            CompilationUnit cu = procedureEntry.getValue();
            createSearch(cu);

            Procedure procedure = new Procedure();
            procedure.name = cu.name;
            procedure.icon = ICON_PREFIX + cu.icon;

            data.procedures.add(procedure);

            if (cu.uses != null && !cu.uses.isEmpty()) {
                for (Member member : cu.members) {
                    if (member.datatype != null)
                        member.datatype = determineFullyQualifiedClassName(cu.uses,
                                member.datatype);

                    if (member.returns != null)
                        member.returns.datatype = determineFullyQualifiedClassName(cu.uses,
                                member.returns.datatype);

                    if (member.parameters != null) {
                        for (Parameter parameter : member.parameters) {
                            parameter.datatype = determineFullyQualifiedClassName(cu.uses,
                                    parameter.datatype);
                        }
                    }
                }
            }
        }

        // Determine class hierarchy, subclasses and search objects
        for (Map.Entry classEntry : classes.entrySet()) {
            CompilationUnit cu = classEntry.getValue();

            createSearch(cu);

            Cls cls = new Cls();
            cls.name = cu.name;
            // Set FullName for inherts classes
            cls.inherits = determineFullyQualifiedClassName(cu.uses, cu.inherits);
            if (!cu.inherits.equals(cls.inherits)) {
                cu.inherits = cls.inherits;
            }
            cls.icon = ICON_PREFIX + cu.icon;

            data.classes.add(cls);

            // Subclasses
            for (Map.Entry subclassEntry : classes.entrySet()) {
                CompilationUnit subclass = subclassEntry.getValue();
                if ("class".equals(subclass.tagname) && cu.name
                        .equals(determineFullyQualifiedClassName(subclass.uses, subclass.inherits)))
                    cu.subclasses.add(subclass.name);
            }

            // Add implementers to the interface
            if (cu.implementations != null && !cu.implementations.isEmpty()) {
                // TempList to avoid conflict
                List listNewImplement = new ArrayList<>();
                Iterator iter = cu.implementations.iterator();

                while (iter.hasNext()) {
                    String currentImplement = iter.next();
                    String fullInterfacePath = determineFullyQualifiedClassName(cu.uses,
                            currentImplement);
                    CompilationUnit iface = classes.get(fullInterfacePath);
                    if (iface != null) {
                        iface.implementers.add(cu.name);
                        // Set FullName for implemented interfaces
                        if (!currentImplement.equals(fullInterfacePath)) {
                            iter.remove();
                            listNewImplement.add(iface.name);
                        }
                    }
                }
                // Adding the Full implement name
                if (!listNewImplement.isEmpty()) {
                    cu.implementations.addAll(listNewImplement);
                }
            }

            // Hierarchy
            HierarchyResult result = new HierarchyResult();
            result = determineClassHierarchy(cu, result);

            List hierarchy = result.getHierarchy();
            Collections.reverse(hierarchy);
            cu.superclasses.addAll(hierarchy);
            cu.superclasses.add(cu.name);

            cu.members.addAll(result.getInheritedmembers());

            if (cu.uses != null && !cu.uses.isEmpty()) {
                for (Member member : cu.members) {
                    if (member.datatype != null)
                        member.datatype = determineFullyQualifiedClassName(cu.uses,
                                member.datatype);

                    if (member.returns != null)
                        member.returns.datatype = determineFullyQualifiedClassName(cu.uses,
                                member.returns.datatype);

                    if (member.parameters != null) {
                        for (Parameter parameter : member.parameters) {
                            parameter.datatype = determineFullyQualifiedClassName(cu.uses,
                                    parameter.datatype);
                        }
                    }
                }
            }

        }

        // Write class js files out
        for (Map.Entry cuEntry : classes.entrySet()) {
            CompilationUnit cu = cuEntry.getValue();

            File baseDir = new File(this.destDirOutput, "classes");

            if (!baseDir.exists()) {
                baseDir.mkdirs();
            }

            File outputFile = new File(baseDir, cu.name + ".js");
            try (OutputStreamWriter file = new OutputStreamWriter(
                    new FileOutputStream(outputFile.toString()), StandardCharsets.UTF_8)) {
                file.write("Ext.data.JsonP." + cu.name.replace(".", "_") + "(" + gson.toJson(cu)
                        + ");");
            } catch (IOException ex) {
                throw new BuildException(ex);
            }
        }

        // Write procedure js files out
        for (Map.Entry procedureEntry : procedures.entrySet()) {
            CompilationUnit cu = procedureEntry.getValue();

            File baseDir = new File(this.destDirOutput, "procedures");

            if (!baseDir.exists()) {
                baseDir.mkdirs();
            }

            String filename = cu.name.replace(".", "_").replace("/", "_");
            File outputFile = new File(baseDir, filename + ".js");
            try (OutputStreamWriter file = new OutputStreamWriter(
                    new FileOutputStream(outputFile.toString()), StandardCharsets.UTF_8)) {
                file.write("Ext.data.JsonP." + filename + "(" + gson.toJson(cu) + ");");
            } catch (IOException ex) {
                throw new BuildException(ex);
            }
        }

        File dataFile = new File(this.destDir, "data.js");
        try (FileWriter file = new FileWriter(dataFile.toString())) {
            file.write("Docs = {\"data\":" + gson.toJson(data) + "}");
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    private HierarchyResult determineClassHierarchy(CompilationUnit curClass,
            HierarchyResult result) {

        String inherits = curClass.inherits;

        if (!"".equals(inherits)) {
            result.addHierarchy(inherits);

            CompilationUnit nextClass = classes.get(inherits);

            if (nextClass != null) {
                for (Member member : nextClass.members) {
                    if (inherits.equals(member.owner) && member.meta.isPrivate == null) {
                        // Happy with a shallow copy here
                        Member inheritedMember = new Member(member);

                        inheritedMember.id = inheritedMember.id.replace(
                                inheritedMember.tagname + "-" + inheritedMember.name,
                                inheritedMember.tagname + "-"
                                        + inheritedMember.owner.replace(".", "_") + "_"
                                        + inheritedMember.name);
                        result.addInheritedmember(inheritedMember);
                    }
                }

                determineClassHierarchy(nextClass, result);
            }
        }
        return result;
    }

    private void copyFullRecursive(File src, File dest) throws IOException {
        if (src.isDirectory()) {
            if (!dest.exists()) {
                dest.mkdirs();
            }

            File[] list = src.listFiles();
            for (File fic : list) {
                copyFullRecursive(fic, new File(dest, fic.getName()));
            }
        } else {
            Files.copy(src.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
        }
    }

    private void extractTemplateDirectory(File outputDir) throws IOException {
        InputStream zipStream = ABLDuck.class.getResourceAsStream("resources/ablduck.zip");
        unzip(zipStream, outputDir);
    }

    public void unzip(InputStream zipStream, File unzipTo) throws IOException {
        if (!unzipTo.exists()) {
            unzipTo.mkdirs();
        }

        try (ZipInputStream zipIn = new ZipInputStream(zipStream)) {
            ZipEntry entry = zipIn.getNextEntry();

            while (entry != null) {
                String filePath = Paths.get(unzipTo.getAbsolutePath(), entry.getName()).toString();
                if (!entry.isDirectory()) {
                    extractFile(zipIn, filePath);
                } else {
                    File dir = new File(filePath);
                    dir.mkdir();
                }
                zipIn.closeEntry();
                entry = zipIn.getNextEntry();
            }
        }
    }

    private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {
            byte[] bytesIn = new byte[BUFFER_SIZE];
            int read = 0;
            while ((read = zipIn.read(bytesIn)) != -1) {
                bos.write(bytesIn, 0, read);
            }
        }
    }

    private void replaceTemplateTags(String tag, String value, java.nio.file.Path file)
            throws IOException {
        Charset charset = StandardCharsets.UTF_8;

        String content = new String(Files.readAllBytes(file), charset);
        content = content.replaceAll(Pattern.quote(tag), Matcher.quoteReplacement(value));
        Files.write(file, content.getBytes(charset));
    }

    public String determineFullyQualifiedClassName(List usings, String partialClassName) {
        if (partialClassName == null || "".equals(partialClassName))
            return partialClassName;

        IDataType idt = DataTypes.getDataType(partialClassName);
        if (idt != null)
            return partialClassName;

        // First check if we have a direct using statement for the datatype
        for (String using : usings) {
            if (using.endsWith("." + partialClassName)) {
                return using;
            }
        }

        // Now check for * using statements
        for (String using : usings) {
            if (using.endsWith("*")) {
                String fullName = using.substring(0, using.length() - 1) + partialClassName;

                if (classes.get(fullName) != null)
                    return fullName;
            }
        }

        return partialClassName;
    }

    public void createSearch(CompilationUnit cu) {
        Search search = new Search();
        search.name = ("class".equals(cu.tagname)
                ? cu.name.substring(cu.name.lastIndexOf('.') + 1)
                : cu.name.substring(cu.name.lastIndexOf('/') + 1));
        search.fullName = cu.name;
        search.icon = ICON_PREFIX + cu.icon;
        search.url = "#!/" + cu.tagname + "/" + cu.name;
        search.sort = 1;
        search.meta = cu.meta;

        data.search.add(search);

        for (Member member : cu.members) {
            search = new Search();
            search.name = member.name;
            search.fullName = ("procedure".equals(cu.tagname) ? cu.name : member.owner) + ":"
                    + member.name;
            search.icon = ICON_PREFIX + member.tagname;
            search.url = "#!/" + cu.tagname + "/"
                    + ("procedure".equals(cu.tagname) ? cu.name : member.owner) + "-"
                    + member.tagname + "-" + member.name;
            search.sort = 3;
            search.meta = member.meta;

            data.search.add(search);
        }
    }

    /**
     * Get the Charset from encoding parameter
     * 
     * @return Charset
     */
    protected Charset getCharset() {
        if (inputCharset == null) {
            try {
                inputCharset = Charset.forName(this.encoding);
            } catch (IllegalArgumentException caught) {
                inputCharset = Charset.defaultCharset();
            }
        }

        return inputCharset;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy