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

io.sarl.maven.docs.AbstractDocumentationMojo Maven / Gradle / Ivy

There is a newer version: 0.12.0
Show newest version
/*
 * $Id: io/sarl/maven/docs/AbstractDocumentationMojo.java v0.10.0 2019-10-26 17:20:53$
 *
 * SARL is an general-purpose agent programming language.
 * More details on http://www.sarl.io
 *
 * Copyright (C) 2014-2019 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 Stéphane Galland
 * @version 0.10.0 2019-10-26 17:20:53
 * @mavengroupid io.sarl.maven
 * @mavenartifactid io.sarl.maven.docs.generator
 * @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 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 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 inferredSourceDirectories;

	/**
	 * The default values for the properties if they are not provided by any other source.
	 */
	@Parameter(required = false)
	private Properties propertyDefaultValues;

	@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 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 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 files, File outputFolder) {
		String firstErrorMessage = null;

		for (final Entry 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.
	 */
	@SuppressWarnings("checkstyle:npathcomplexity")
	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.addLowPropertyProvider(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.addLowPropertyProvider(createProjectProperties());
		internalParser.addLowPropertyProvider(this.session.getCurrentProject().getProperties());
		internalParser.addLowPropertyProvider(this.session.getUserProperties());
		internalParser.addLowPropertyProvider(this.session.getSystemProperties());
		internalParser.addLowPropertyProvider(createGeneratorProperties());
		final Properties defaultValues = createDefaultValueProperties();
		if (defaultValues != null) {
			internalParser.addLowPropertyProvider(defaultValues);
		}
		return parser;
	}

	private Properties createDefaultValueProperties() {
		return this.propertyDefaultValues;
	}

	private Properties createProjectProperties() {
		final Properties props = new Properties();
		final MavenProject prj = this.session.getCurrentProject();
		props.put("project.groupId", Strings.emptyIfNull(prj.getGroupId())); //$NON-NLS-1$
		props.put("project.artifactId", Strings.emptyIfNull(prj.getArtifactId())); //$NON-NLS-1$
		props.put("project.basedir", prj.getBasedir() != null ? prj.getBasedir().getAbsolutePath() : ""); //$NON-NLS-1$ //$NON-NLS-2$
		props.put("project.description", Strings.emptyIfNull(prj.getDescription())); //$NON-NLS-1$
		props.put("project.id", Strings.emptyIfNull(prj.getId())); //$NON-NLS-1$
		props.put("project.inceptionYear", Strings.emptyIfNull(prj.getInceptionYear())); //$NON-NLS-1$
		props.put("project.name", Strings.emptyIfNull(prj.getName())); //$NON-NLS-1$
		props.put("project.version", Strings.emptyIfNull(prj.getVersion())); //$NON-NLS-1$
		props.put("project.url", Strings.emptyIfNull(prj.getUrl())); //$NON-NLS-1$
		props.put("project.encoding", Strings.emptyIfNull(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", Strings.emptyIfNull(descriptor.getArtifactId())); //$NON-NLS-1$
		props.put("generator.version", Strings.emptyIfNull(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 getFiles() {
		final Map 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.fileTraverser().breadthFirst(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 getClassPath() throws IOException {
		final Set 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 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")), includes, excludes); //$NON-NLS-1$
			} 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;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy