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

com.arsframework.plugin.apidoc.BasicMojo Maven / Gradle / Ivy

The newest version!
package com.arsframework.plugin.apidoc;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.arsframework.apidoc.core.Api;
import com.arsframework.apidoc.core.Configuration;
import com.arsframework.apidoc.core.ContextHelper;
import com.arsframework.apidoc.core.DocumentHelper;
import com.arsframework.apidoc.core.MethodAnalyser;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.RootDoc;
import com.sun.tools.javadoc.Main;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.BuildPluginManager;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.Xpp3DomUtils;

import static com.arsframework.plugin.apidoc.XmlHelper.configuration;
import static com.arsframework.plugin.apidoc.XmlHelper.element;
import static com.arsframework.plugin.apidoc.XmlHelper.toXpp3Dom;

/**
 * Basic mojo
 *
 * @author Woody
 */
@Execute(phase = LifecyclePhase.COMPILE)
public abstract class BasicMojo extends AbstractMojo {
    /**
     * Source file suffix
     */
    private static final String SOURCE_FILE_SUFFIX = ".java";

    /**
     * Package definition name
     */
    private static final String PACKAGE_DEFINITION_NAME = "package ";

    /**
     * package-info definition name
     */
    private static final String PACKAGE_INFO_DEFINITION_NAME = "package-info.java";

    /**
     * Classpath separator
     */
    private static final String CLASSPATH_SEPARATOR = System.getProperty("path.separator");

    @Component
    protected BuildPluginManager manager;

    @Parameter(defaultValue = "${session}", readonly = true, required = true)
    protected MavenSession session;

    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    protected MavenProject project;

    @Parameter(defaultValue = "${project.basedir}/target/apidoc", required = true)
    protected String output;

    @Parameter(defaultValue = "${project.compileClasspathElements}", readonly = true, required = true)
    protected List compileDirectories;

    @Parameter(defaultValue = "${project.build.directory}/sources", readonly = true, required = true)
    protected String dependencySourceDirectory;

    @Parameter(defaultValue = "${project.groupId}", required = true)
    protected String includeGroupIdentities;

    /**
     * Whether the date is displayed
     */
    @Parameter(defaultValue = "false", required = true)
    protected boolean displayDate;

    /**
     * Whether the author is displayed
     */
    @Parameter(defaultValue = "false", required = true)
    protected boolean displayAuthor;

    /**
     * Whether the sample request is enabled
     */
    @Parameter(defaultValue = "true", required = true)
    protected boolean enableSampleRequest;

    /**
     * Whether the response example is enabled
     */
    @Parameter(defaultValue = "true", required = true)
    protected boolean enableResponseExample;

    /**
     * Whether the snake and underline conversion is enabled
     */
    @Parameter(defaultValue = "false", required = true)
    protected boolean enableSnakeUnderlineConversion;

    /**
     * Include header names
     */
    @Parameter
    protected List includeHeaders;

    /**
     * Exclude class names
     */
    @Parameter
    protected List excludeClasses;

    /**
     * Api analyser factory class
     */
    @Parameter
    protected String analyserFactoryClass;

    /**
     * Class and source mappings
     */
    private final Map, String> sources = new LinkedHashMap<>();

    /**
     * Source file name and class document mappings
     */
    private final Map documents = new HashMap<>();

    /**
     * Initialize class loader
     *
     * @return URL class loader
     * @throws IOException IO exception
     */
    private URLClassLoader initializeClassLoader() throws IOException {
        Set artifacts = this.project.getArtifacts();
        URL[] urls = new URL[this.compileDirectories.size() + artifacts.size()];
        int i = 0;
        for (String directory : this.compileDirectories) {
            urls[i++] = new File(directory).toURI().toURL();
        }
        for (Artifact artifact : artifacts) {
            urls[i++] = artifact.getFile().toURI().toURL();
        }
        return new URLClassLoader(urls, this.getClass().getClassLoader());
    }

    /**
     * Initialize class of source directory
     *
     * @param directory Source directory
     */
    private void initializeClasses(File directory) {
        for (File file : DocumentHelper.listDirectoryFiles(directory)) {
            if (file.isDirectory()) {
                this.initializeClasses(file);
            } else if (file.getName().endsWith(SOURCE_FILE_SUFFIX)
                    && !file.getName().equalsIgnoreCase(PACKAGE_INFO_DEFINITION_NAME)) {
                try {
                    this.sources.put(this.loadClass(file), file.getPath());
                } catch (IOException | ClassNotFoundException e) {
                    this.getLog().warn("Class loading failed: " + e.getMessage());
                }
            }
        }
    }

    /**
     * Load class by source file
     *
     * @param file Source file
     * @return Class object
     * @throws IOException            IO exception
     * @throws ClassNotFoundException Class not found exception
     */
    private Class loadClass(File file) throws IOException, ClassNotFoundException {
        Objects.requireNonNull(file, "file not specified");
        String name = file.getName();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (!(line = line.trim()).isEmpty() && line.startsWith(PACKAGE_DEFINITION_NAME)) {
                    name = (line.split("[ ;]")[1]).concat(".").concat(name);
                    break;
                }
            }
        }
        return ContextHelper.getClassLoader().loadClass(name.substring(0, name.lastIndexOf('.')));
    }

    /**
     * Unpack dependencies
     *
     * @throws MojoExecutionException Mojo execution exception
     */
    private void unpackDependencies() throws MojoExecutionException {
        Plugin plugin = this.project.getPluginManagement().getPluginsAsMap()
                .get("org.apache.maven.plugins:maven-dependency-plugin");
        if (plugin == null) {
            throw new MojoExecutionException("Dependency plugin could not be found in ");
        }
        try {
            MojoDescriptor descriptor = this.manager.loadPlugin(plugin, this.project.getRemotePluginRepositories(),
                    this.session.getRepositorySession()).getMojo("unpack-dependencies");
            Xpp3Dom configuration = Xpp3DomUtils.mergeXpp3Dom(configuration(
                    element("classifier", "sources"),
                    element("includeScope", "compile"),
                    element("includeGroupIds", String.join(",", ContextHelper.getIncludeGroupIdentities())),
                    element("failOnMissingClassifierArtifact", "false"),
                    element("outputDirectory", this.dependencySourceDirectory),
                    element("markersDirectory", this.dependencySourceDirectory)
            ), toXpp3Dom(descriptor.getMojoConfiguration()));
            this.manager.executeMojo(this.session, new MojoExecution(descriptor, configuration));
        } catch (Exception e) {
            throw new MojoExecutionException("Unpack dependencies failed", e);
        }
    }

    /**
     * Unpack project sources
     *
     * @throws IOException IO exception
     */
    private void unpackProjectSources() throws IOException {
        List roots = this.project.getCompileSourceRoots();
        if (roots != null && !roots.isEmpty()) {
            for (String root : roots) {
                DocumentHelper.copyDirectory(new File(root), new File(this.dependencySourceDirectory));
            }
        }
    }

    /**
     * Build method analyser factory
     *
     * @return Method analyser factory
     * @throws ReflectiveOperationException Reflective operation exception
     */
    protected MethodAnalyser.Factory buildMethodAnalyserFactory() throws ReflectiveOperationException {
        String type;
        if (this.analyserFactoryClass == null || (type = this.analyserFactoryClass.trim()).isEmpty()) {
            return MethodAnalyser::new;
        }
        return (MethodAnalyser.Factory) Class.forName(type, true, ContextHelper.getClassLoader()).newInstance();
    }

    /**
     * Initialize
     *
     * @throws IOException            IO exception
     * @throws MojoExecutionException Mojo execution exception
     */
    protected void initialize() throws IOException, MojoExecutionException {
        // Class loader
        URLClassLoader classLoader = this.initializeClassLoader();
        ContextHelper.setClassLoader(classLoader);

        // Class path
        String classpath = String.join(CLASSPATH_SEPARATOR,
                Stream.of(classLoader.getURLs()).map(URL::getPath).toArray(String[]::new));
        ContextHelper.setClasspath(classpath);

        // Configuration
        Configuration configuration = Configuration.builder().displayDate(this.displayDate)
                .displayAuthor(this.displayAuthor).enableSampleRequest(this.enableSampleRequest)
                .enableResponseExample(this.enableResponseExample)
                .enableSnakeUnderlineConversion(this.enableSnakeUnderlineConversion).build();
        ContextHelper.setConfiguration(configuration);

        // Include group identities
        Set groups = Stream.of(this.includeGroupIdentities.split("[, ]"))
                .filter(group -> group != null && !group.isEmpty()).collect(Collectors.toSet());
        groups.add(this.project.getGroupId());
        ContextHelper.setIncludeGroupIdentities(groups);

        // Document provider
        ContextHelper.setDocumentProvider(clazz -> {
            Objects.requireNonNull(clazz, "clazz not specified");
            String name = clazz.getName().replace("$", ".");
            ClassDoc document = this.documents.get(name);
            if (document == null) {
                String source = this.sources.get(clazz);
                if (source == null && (clazz = clazz.getDeclaringClass()) != null) {
                    source = this.sources.get(clazz);
                }
                if (source == null) {
                    return null;
                }
                Main.execute(classLoader, "-doclet", Doclet.class.getName(), "-quiet", "-encoding", "utf-8",
                        "-sourcepath", this.dependencySourceDirectory, "-classpath", classpath, source);
                if (Doclet.root != null) {
                    for (ClassDoc doc : Doclet.root.classes()) {
                        if (doc.toString().equals(name)) {
                            document = doc;
                        }
                        this.documents.put(doc.toString(), doc);
                    }
                }
            }
            return document;
        });

        // Unpack dependencies
        this.unpackDependencies();

        // Unpack project sources
        this.unpackProjectSources();

        // initialize classes
        this.initializeClasses(new File(this.dependencySourceDirectory));
    }

    /**
     * Get apis
     *
     * @param factory Method analyser factory
     * @return Api list
     */
    protected List getApis(MethodAnalyser.Factory factory) {
        Objects.requireNonNull(factory, "factory not specified");
        return this.sources.keySet().stream().filter(clazz -> DocumentHelper.isApiClass(clazz)
                && (this.excludeClasses == null
                || this.excludeClasses.stream().noneMatch(clazz.getName()::startsWith))).flatMap(clazz -> {
            try {
                return Stream.of(clazz.getDeclaredMethods()).filter(DocumentHelper::isApiMethod)
                        .map(method -> factory.build(method).parse());
            } catch (Throwable e) {
                this.getLog().warn("Api loading failed: " + e.getMessage());
            }
            return null;
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }

    /**
     * Build the document with apis
     *
     * @param path File path
     * @param apis Api list
     * @throws Exception Exception
     */
    protected abstract void building(File path, List apis) throws Exception;

    @Override
    public final void execute() throws MojoExecutionException {
        try {
            this.initialize();
            List apis = this.getApis(this.buildMethodAnalyserFactory());
            if (apis != null && !apis.isEmpty()) {
                File path = new File(this.output);
                if (!path.exists()) {
                    path.mkdirs();
                }
                this.building(path, apis);
            }
        } catch (Exception e) {
            throw new MojoExecutionException(e.getMessage(), e);
        } finally {
            ContextHelper.clear();
            DocumentHelper.removeDirectory(new File(this.dependencySourceDirectory));
        }
    }

    /**
     * Document doclet
     */
    public static final class Doclet {
        /**
         * Root document
         */
        private static RootDoc root;

        /**
         * Receive the root document
         *
         * @param root Root document
         * @return true/false
         */
        public static boolean start(RootDoc root) {
            Doclet.root = root;
            return true;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy