io.sarl.maven.docs.AbstractDocumentationMojo.java Source code

Java tutorial

Introduction

Here is the source code for io.sarl.maven.docs.AbstractDocumentationMojo.java

Source

/*
 * $Id$
 *
 * SARL is an general-purpose agent programming language.
 * More details on http://www.sarl.io
 *
 * Copyright (C) 2014-2017 the original authors or authors.
 *
 * 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 io.sarl.maven.docs;

import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;

import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.name.Names;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.io.DirectoryScanner;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.apache.maven.toolchain.ToolchainPrivate;
import org.apache.maven.toolchain.java.JavaToolChain;
import org.arakhne.afc.vmutil.FileSystem;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.eclipse.xtext.Constants;
import org.eclipse.xtext.util.JavaVersion;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.xbase.lib.util.ReflectExtensions;

import io.sarl.maven.docs.markdown.MarkdownParser;
import io.sarl.maven.docs.parser.AbstractMarkerLanguageParser;
import io.sarl.maven.docs.parser.SarlDocumentationParser;
import io.sarl.maven.docs.parser.SarlDocumentationParser.ParsingException;
import io.sarl.maven.docs.testing.DocumentationSetup;
import io.sarl.maven.docs.testing.ScriptExecutor;

/** Abstract Maven MOJO for the documentation of the SARL project.
 *
 * @author $Author: sgalland$
 * @version $FullVersion$
 * @mavengroupid $GroupId$
 * @mavenartifactid $ArtifactId$
 * @since 0.6
 */
public abstract class AbstractDocumentationMojo extends AbstractMojo {

    /** Name of the default source directory.
     */
    public static final String DEFAULT_SOURCE_DIRECTORY = "src/main/documentation"; //$NON-NLS-1$

    /**
     * Location of the temp directory.
     */
    @Parameter(defaultValue = "${basedir}/target/documentation-temp", required = true)
    protected File tempDirectory;

    /**
     * Location of the generated folder of the test sources.
     */
    @Parameter(defaultValue = "${basedir}/target/generated-documentation-sources", required = true)
    protected String testSourceDirectory;

    /**
     * Location of the source directories.
     */
    @Parameter
    protected List<String> sourceDirectories;

    /** Default file encoding.
     */
    @Parameter(defaultValue = "${project.build.sourceEncoding}", required = true)
    protected String encoding;

    /**
     * Indicates if the source directories provided by Maven
     * must be ignored if {@link #sourceDirectories} are manually provided.
     */
    @Parameter(defaultValue = "true", required = true)
    protected boolean overrideSourceDirectories;

    /**
     * The project itself. This parameter is set by maven.
     */
    @Parameter(required = true, defaultValue = "${project}", readonly = true)
    protected MavenProject project;

    /**
     * The base directory.
     */
    @Parameter(required = true, defaultValue = "${basedir}", readonly = true)
    protected File baseDirectory;

    /**
     * File extensions, including the dot character. Default is the file extension of the Markdown format.
     */
    @Parameter(required = false)
    protected List<String> fileExtensions;

    /** File extension for the target language.
     */
    protected String targetLanguageFileExtension;

    /**
     * The current Maven session.
     */
    @Parameter(defaultValue = "${session}", required = true, readonly = true)
    protected MavenSession session;

    /**
     * Indicates if the Github extension should be enabled.
     */
    @Parameter(defaultValue = "false", required = false)
    protected boolean githubExtension;

    /**
     * Indicates if the line continuation syntax is enabled or not.
     */
    @Parameter(defaultValue = "true", required = false)
    protected boolean isLineContinuationEnable;

    /**
     * Java version number to support.
     */
    @Parameter(required = false)
    protected String source;

    /** Injector.
     */
    protected Injector injector;

    /** Inferred source directories.
     */
    protected List<String> inferredSourceDirectories;

    @Component
    private ToolchainManager toolchainManager;

    private ReflectExtensions reflect;

    private static boolean isFileExtension(File filename, String[] extensions) {
        final String extension = FileSystem.extension(filename);
        for (final String ext : extensions) {
            if (Strings.equal(ext, extension)) {
                return true;
            }
        }
        return false;
    }

    /** Replies the message to display for skipping the execution of this mojo.
     * If this function does not reply a message, the execution is not skipped.
     * If this function replies a message, the execution is skipped.
     *
     * @return A message for skipping the execution, or {@code null} for executing the mojo.
     */
    protected abstract String getSkippingMessage();

    @Override
    public final void execute() throws MojoExecutionException {
        final String skipMessage = getSkippingMessage();
        if (!Strings.isEmpty(skipMessage)) {
            getLog().info(skipMessage);
            return;
        }
        if (this.fileExtensions == null || this.fileExtensions.isEmpty()) {
            this.fileExtensions = Arrays.asList(MarkdownParser.MARKDOWN_FILE_EXTENSIONS);
        }

        if (this.sourceDirectories == null) {
            this.sourceDirectories = Collections.emptyList();
        }
        if (!this.sourceDirectories.isEmpty() && this.overrideSourceDirectories) {
            this.inferredSourceDirectories = Lists.newArrayList(this.sourceDirectories);
        } else {
            this.inferredSourceDirectories = Lists.newArrayList(this.project.getCompileSourceRoots());
            this.inferredSourceDirectories.addAll(this.sourceDirectories);
            this.inferredSourceDirectories.add(DEFAULT_SOURCE_DIRECTORY);
        }

        getLog().info(Messages.AbstractDocumentationMojo_0);
        this.injector = DocumentationSetup.doSetup();
        assert this.injector != null;

        if (this.reflect == null) {
            this.reflect = this.injector.getInstance(ReflectExtensions.class);
        }

        this.targetLanguageFileExtension = this.injector
                .getInstance(Key.get(String.class, Names.named(Constants.FILE_EXTENSIONS)));

        final String errorMessage = internalExecute();
        if (!Strings.isEmpty(errorMessage)) {
            throw new MojoExecutionException(errorMessage);
        }
    }

    /** Internal run.
     *
     * @return the error message
     */
    protected String internalExecute() {
        getLog().info(Messages.AbstractDocumentationMojo_1);
        final Map<File, File> files = getFiles();
        getLog().info(MessageFormat.format(Messages.AbstractDocumentationMojo_2, files.size()));
        return internalExecute(files);
    }

    /** Internal run.
     *
     * @param files the map from source file to the source folder.
     * @return the error message
     */
    @SuppressWarnings("static-method")
    protected String internalExecute(Map<File, File> files) {
        throw new UnsupportedOperationException();
    }

    /** Execute the mojo on the given set of files.
     *
     * @param files the files
     * @param outputFolder the output directory.
     * @return the error message
     */
    protected String internalExecute(Map<File, File> files, File outputFolder) {
        String firstErrorMessage = null;

        for (final Entry<File, File> entry : files.entrySet()) {
            final File inputFile = entry.getKey();
            try {
                final AbstractMarkerLanguageParser parser = createLanguageParser(inputFile);
                final File sourceFolder = entry.getValue();
                final File relativePath = FileSystem.makeRelative(inputFile, sourceFolder);
                internalExecute(sourceFolder, inputFile, relativePath, outputFolder, parser);
            } catch (Throwable exception) {
                final String errorMessage = formatErrorMessage(inputFile, exception);
                getLog().error(errorMessage);
                if (Strings.isEmpty(firstErrorMessage)) {
                    firstErrorMessage = errorMessage;
                }
                getLog().debug(exception);
            }
        }
        return firstErrorMessage;
    }

    /** Execute the mojo on the given set of files.
     *
     * @param sourceFolder the source folder.
     * @param inputFile the input file.
     * @param relativeInputFile the name of the input file relatively to the source folder.
     * @param outputFolder the output folder.
     * @param parser the parser to be used for reading the input file.
     * @throws IOException if there is some issue with IO.
     */
    @SuppressWarnings("static-method")
    protected void internalExecute(File sourceFolder, File inputFile, File relativeInputFile, File outputFolder,
            AbstractMarkerLanguageParser parser) throws IOException {
        throw new UnsupportedOperationException();
    }

    /** Format the error message.
     *
     * @param inputFile the input file.
     * @param exception the error.
     * @return the error message.
     */
    protected String formatErrorMessage(File inputFile, Throwable exception) {
        File filename;
        int lineno = 0;
        final boolean addExceptionName;
        if (exception instanceof ParsingException) {
            addExceptionName = false;
            final ParsingException pexception = (ParsingException) exception;
            final File file = pexception.getFile();
            if (file != null) {
                filename = file;
            } else {
                filename = inputFile;
            }
            lineno = pexception.getLineno();
        } else {
            addExceptionName = true;
            filename = inputFile;
        }
        for (final String sourceDir : this.session.getCurrentProject().getCompileSourceRoots()) {
            final File root = new File(sourceDir);
            if (isParentFile(filename, root)) {
                try {
                    filename = FileSystem.makeRelative(filename, root);
                } catch (IOException exception1) {
                    //
                }
                break;
            }
        }
        final StringBuilder msg = new StringBuilder();
        msg.append(filename.toString());
        if (lineno > 0) {
            msg.append(":").append(lineno); //$NON-NLS-1$
        }
        msg.append(": "); //$NON-NLS-1$
        final Throwable rootEx = Throwables.getRootCause(exception);
        if (addExceptionName) {
            msg.append(rootEx.getClass().getName());
            msg.append(" - "); //$NON-NLS-1$
        }
        msg.append(rootEx.getLocalizedMessage());
        return msg.toString();
    }

    private static boolean isParentFile(File file, File root) {
        if (file.isAbsolute() && root.isAbsolute()) {
            try {
                final String[] components1 = FileSystem.split(file.getCanonicalFile());
                final String[] components2 = FileSystem.split(root.getCanonicalFile());
                for (int i = 0; i < components2.length; ++i) {
                    if (i >= components1.length || !Strings.equal(components2[i], components1[i])) {
                        return false;
                    }
                }
                return true;
            } catch (IOException exception) {
                //
            }
        }
        return false;
    }

    /** Create a parser for the given file.
     *
     * @param inputFile the file to be parsed.
     * @return the parser.
     * @throws MojoExecutionException if the parser cannot be created.
     * @throws IOException if a classpath entry cannot be found.
     */
    protected AbstractMarkerLanguageParser createLanguageParser(File inputFile)
            throws MojoExecutionException, IOException {
        final AbstractMarkerLanguageParser parser;
        if (isFileExtension(inputFile, MarkdownParser.MARKDOWN_FILE_EXTENSIONS)) {
            parser = this.injector.getInstance(MarkdownParser.class);
        } else {
            throw new MojoExecutionException(MessageFormat.format(Messages.AbstractDocumentationMojo_3, inputFile));
        }
        parser.setGithubExtensionEnable(this.githubExtension);

        final SarlDocumentationParser internalParser = parser.getDocumentParser();

        if (this.isLineContinuationEnable) {
            internalParser.setLineContinuation(SarlDocumentationParser.DEFAULT_LINE_CONTINUATION);
        } else {
            internalParser.addPropertyProvider(createProjectProperties());
        }

        final ScriptExecutor scriptExecutor = internalParser.getScriptExecutor();
        final StringBuilder cp = new StringBuilder();
        for (final File cpElement : getClassPath()) {
            if (cp.length() > 0) {
                cp.append(":"); //$NON-NLS-1$
            }
            cp.append(cpElement.getAbsolutePath());
        }
        scriptExecutor.setClassPath(cp.toString());
        final String bootPath = getBootClassPath();
        if (!Strings.isEmpty(bootPath)) {
            scriptExecutor.setBootClassPath(bootPath);
        }
        JavaVersion version = null;
        if (!Strings.isEmpty(this.source)) {
            version = JavaVersion.fromQualifier(this.source);
        }
        if (version == null) {
            version = JavaVersion.JAVA8;
        }
        scriptExecutor.setJavaSourceVersion(version.getQualifier());
        scriptExecutor.setTempFolder(this.tempDirectory.getAbsoluteFile());

        internalParser.addPropertyProvider(createProjectProperties());
        internalParser.addPropertyProvider(this.session.getCurrentProject().getProperties());
        internalParser.addPropertyProvider(this.session.getUserProperties());
        internalParser.addPropertyProvider(this.session.getSystemProperties());
        internalParser.addPropertyProvider(createGeneratorProperties());
        return parser;
    }

    private Properties createProjectProperties() {
        final Properties props = new Properties();
        final MavenProject prj = this.session.getCurrentProject();
        props.put("project.groupId", prj.getGroupId()); //$NON-NLS-1$
        props.put("project.artifactId", prj.getArtifactId()); //$NON-NLS-1$
        props.put("project.basedir", prj.getBasedir()); //$NON-NLS-1$
        props.put("project.description", prj.getDescription()); //$NON-NLS-1$
        props.put("project.id", prj.getId()); //$NON-NLS-1$
        props.put("project.inceptionYear", prj.getInceptionYear()); //$NON-NLS-1$
        props.put("project.name", prj.getName()); //$NON-NLS-1$
        props.put("project.version", prj.getVersion()); //$NON-NLS-1$
        props.put("project.url", prj.getUrl()); //$NON-NLS-1$
        props.put("project.encoding", this.encoding); //$NON-NLS-1$
        return props;
    }

    private Properties createGeneratorProperties() {
        final Properties props = new Properties();
        final PluginDescriptor descriptor = (PluginDescriptor) getPluginContext().get("pluginDescriptor"); //$NON-NLS-1$
        props.put("generator.name", descriptor.getArtifactId()); //$NON-NLS-1$
        props.put("generator.version", descriptor.getVersion()); //$NON-NLS-1$
        return props;
    }

    /** Replies the source files.
     *
     * @return the map from the source files to the corresponding source folders.
     */
    protected Map<File, File> getFiles() {
        final Map<File, File> files = new TreeMap<>();
        for (final String rootName : this.inferredSourceDirectories) {
            File root = FileSystem.convertStringToFile(rootName);
            if (!root.isAbsolute()) {
                root = FileSystem.makeAbsolute(root, this.baseDirectory);
            }
            getLog().debug(MessageFormat.format(Messages.AbstractDocumentationMojo_4, root.getName()));
            for (final File file : Files.fileTreeTraverser().breadthFirstTraversal(root)) {
                if (file.exists() && file.isFile() && !file.isHidden() && file.canRead() && hasExtension(file)) {
                    files.put(file, root);
                }
            }
        }
        return files;
    }

    private boolean hasExtension(File file) {
        final String extension = FileSystem.extension(file);
        return this.fileExtensions.contains(extension);
    }

    /** Convert a file to a package name.
     *
     * @param rootPackage an additional root package.
     * @param packageName the file.
     * @return the package name.
     */
    protected static String toPackageName(String rootPackage, File packageName) {
        final StringBuilder name = new StringBuilder();
        File tmp = packageName;
        while (tmp != null) {
            final String elementName = tmp.getName();
            if (!Strings.equal(FileSystem.CURRENT_DIRECTORY, elementName)
                    && !Strings.equal(FileSystem.PARENT_DIRECTORY, elementName)) {
                if (name.length() > 0) {
                    name.insert(0, "."); //$NON-NLS-1$
                }
                name.insert(0, elementName);
            }
            tmp = tmp.getParentFile();
        }
        if (!Strings.isEmpty(rootPackage)) {
            if (name.length() > 0) {
                name.insert(0, "."); //$NON-NLS-1$
            }
            name.insert(0, rootPackage);
        }
        return name.toString();
    }

    /** Convert a a package name for therelative file.
     *
     * @param packageName the name.
     * @return the file.
     */
    protected static File toPackageFolder(String packageName) {
        File file = null;
        for (final String element : packageName.split("[.]")) { //$NON-NLS-1$
            if (file == null) {
                file = new File(element);
            } else {
                file = new File(file, element);
            }
        }
        return file;
    }

    /** Replies the current classpath.
     *
     * @return the current classpath.
     * @throws IOException on failure.
     */
    protected List<File> getClassPath() throws IOException {
        final Set<String> classPath = new LinkedHashSet<>();
        classPath.add(this.session.getCurrentProject().getBuild().getSourceDirectory());
        try {
            classPath.addAll(this.session.getCurrentProject().getCompileClasspathElements());
        } catch (DependencyResolutionRequiredException e) {
            throw new IOException(e.getLocalizedMessage(), e);
        }
        for (final Artifact dep : this.session.getCurrentProject().getArtifacts()) {
            classPath.add(dep.getFile().getAbsolutePath());
        }
        classPath.remove(this.session.getCurrentProject().getBuild().getOutputDirectory());
        final List<File> files = new ArrayList<>();
        for (final String filename : classPath) {
            final File file = new File(filename);
            if (file.exists()) {
                files.add(file);
            }
        }
        return files;
    }

    /** Replies the boot classpath.
     *
     * @return the boot classpath.
     * @throws IOException in case of error.
     */
    protected String getBootClassPath() throws IOException {
        final Toolchain toolchain = this.toolchainManager.getToolchainFromBuildContext("jdk", this.session); //$NON-NLS-1$
        if (toolchain instanceof JavaToolChain && toolchain instanceof ToolchainPrivate) {
            final JavaToolChain javaToolChain = (JavaToolChain) toolchain;
            final ToolchainPrivate privateJavaToolChain = (ToolchainPrivate) toolchain;
            String[] includes = { "jre/lib/*", "jre/lib/ext/*", "jre/lib/endorsed/*" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            String[] excludes = new String[0];
            final Xpp3Dom config = (Xpp3Dom) privateJavaToolChain.getModel().getConfiguration();
            if (config != null) {
                final Xpp3Dom bootClassPath = config.getChild("bootClassPath"); //$NON-NLS-1$
                if (bootClassPath != null) {
                    final Xpp3Dom includeParent = bootClassPath.getChild("includes"); //$NON-NLS-1$
                    if (includeParent != null) {
                        includes = getValues(includeParent.getChildren("include")); //$NON-NLS-1$
                    }
                    final Xpp3Dom excludeParent = bootClassPath.getChild("excludes"); //$NON-NLS-1$
                    if (excludeParent != null) {
                        excludes = getValues(excludeParent.getChildren("exclude")); //$NON-NLS-1$
                    }
                }
            }

            try {
                return scanBootclasspath(Objects.toString(this.reflect.invoke(javaToolChain, "getJavaHome")), //$NON-NLS-1$
                        includes, excludes);
            } catch (Exception e) {
                throw new IOException(e.getLocalizedMessage(), e);
            }
        }
        return ""; //$NON-NLS-1$
    }

    private static String scanBootclasspath(String javaHome, String[] includes, String[] excludes) {
        final DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir(new File(javaHome));
        scanner.setIncludes(includes);
        scanner.setExcludes(excludes);
        scanner.scan();

        final StringBuilder bootClassPath = new StringBuilder();
        final String[] includedFiles = scanner.getIncludedFiles();
        for (int i = 0; i < includedFiles.length; i++) {
            if (i > 0) {
                bootClassPath.append(File.pathSeparator);
            }
            bootClassPath.append(new File(javaHome, includedFiles[i]).getAbsolutePath());
        }
        return bootClassPath.toString();
    }

    private static String[] getValues(Xpp3Dom[] children) {
        final String[] values = new String[children.length];
        for (int i = 0; i < values.length; i++) {
            values[i] = children[i].getValue();
        }
        return values;
    }

}