/*
* Copyright (C) 2011 4th Line GmbH, Switzerland
*
* 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/>.
*/
package org.fourthline.lemma.maven;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.codehaus.classworlds.ClassRealm;
import org.codehaus.classworlds.ClassWorld;
import org.codehaus.plexus.util.FileUtils;
import org.seamless.util.io.IO;
import org.seamless.util.logging.LoggingUtil;
import org.seamless.xhtml.XHTML;
import org.fourthline.lemma.pipeline.javadoc.XHTMLTemplateJavadocPipeline;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
/**
* @author Christian Bauer
* @goal manual
* @requiresDependencyResolution test
*/
public class LemmaMojo extends AbstractMojo {
/**
* @parameter expression="${manual.sourceDirectories}"
* description="Don't use project.build.testSourceDirectory but the given source directories."
*/
protected List<File> sourceDirectories;
/**
* @parameter expression="${manual.manualSourceDirectory}"
* default-value="${basedir}/src/manual"
* description="The directory containing manual template and (CSS/Image) resources."
*/
protected File manualSourceDirectory;
/**
* @parameter description="Included package, repeat option for multiple packages."
*/
protected List<String> packageNames = new ArrayList();
/**
* @parameter expression="${manual.templateFilename}"
* default-value="${project.artifactId}-manual.xhtml"
*/
protected String templateFilename;
/**
* @parameter expression="${manual.outputFilename}"
* default-value="${project.artifactId}-manual"
*/
protected String outputFilename;
/**
* @parameter expression="${manual.outputPath}"
* default-value="manual"
*/
protected String outputPath;
/**
* @parameter expression="${manual.renameXHTMLFiles}"
* default-value="true"
*/
protected boolean renameXHTMLFiles;
/**
* @parameter expression="${manual.processXRefs}"
* default-value="true"
*/
protected boolean processXRefs;
/**
* @parameter
*/
protected List<String> deleteSiteFiles = new ArrayList();
/**
* @parameter expression="${project}"
* @required
* @readonly
*/
protected MavenProject project;
public MavenProject getProject() {
return project;
}
// Maven's plugin classloader does not see the project's build/test output by default, see
// http://maven.apache.org/guides/mini/guide-maven-classloading.html
public void extendPluginClasspath(List<String> elements) throws MojoExecutionException {
// I found most of this on pastebin
ClassWorld world = new ClassWorld();
ClassRealm realm;
try {
realm = world.newRealm(
"maven.plugin." + getClass().getSimpleName(),
Thread.currentThread().getContextClassLoader()
);
for (String element : elements) {
File elementFile = new File(element);
getLog().debug("Adding element to plugin classpath" + elementFile.getPath());
URL url = new URL("file:///" + elementFile.getPath() + (elementFile.isDirectory() ? "/" : ""));
realm.addConstituent(url);
}
} catch (Exception ex) {
throw new MojoExecutionException(ex.toString(), ex);
}
Thread.currentThread().setContextClassLoader(realm.getClassLoader());
}
public void execute() throws MojoExecutionException, MojoFailureException {
try {
File templateFile = new File(manualSourceDirectory, templateFilename);
if (!templateFile.exists()) {
boolean enable = Boolean.parseBoolean(project.getProperties().getProperty("lemma.manual", "true"));
if (!enable || project.getPackaging().equals("pom")) {
return;
}
throw new Exception("Configured template not found in manual directory: " + templateFile);
}
// We might want to load stuff from the test classpath
extendPluginClasspath((List<String>)project.getTestClasspathElements());
// Default to test source directory if no source directories are configured
if (sourceDirectories.isEmpty()) {
File testSourceDirectory = new File(project.getBuild().getTestSourceDirectory());
sourceDirectories.add(testSourceDirectory);
getLog().info(">>> Generating documentation using test source directory: " + testSourceDirectory);
} else {
getLog().info(">>> Generating documentation using multiple source directories:");
for (File sourceDirectory : sourceDirectories) {
getLog().info(sourceDirectory.toString());
}
}
XHTMLTemplateJavadocPipeline pipeline = createPipeline(sourceDirectories, packageNames, project);
XHTML result = pipeline.execute(templateFile);
String path = IO.makeRelativePath(outputPath, project.getBuild().getDirectory());
File outputFile = new File(project.getBuild().getDirectory() + "/" + path, outputFilename + ".xhtml");
pipeline.prepareOutputFile(outputFile, true);
getLog().info("Writing output file: " + outputFile.getAbsolutePath());
IO.writeUTF8(
outputFile,
pipeline.getParser().print(result, 4, true)
);
copyManualResources(new File(project.getBuild().getDirectory(), path));
copyDocFiles(new File(project.getBuild().getDirectory(), path));
} catch (Exception ex) {
throw new MojoExecutionException("Error occured: " + ex.getMessage(), ex);
}
}
public XHTMLTemplateJavadocPipeline createPipeline(List<File> sourceDirectories,
List<String> packageNames,
MavenProject project) throws Exception {
// Yep, the Javadoc tool has its own classpath, we abuse the javac system property to get it into the Gaftercode
String javadocClasspath = null;
try {
List<String> classpathElements = (List<String>) project.getTestClasspathElements();
StringBuilder sb = new StringBuilder();
for (String classpathElement : classpathElements) {
sb.append(classpathElement).append(File.pathSeparator);
}
if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1);
javadocClasspath = sb.toString();
if (javadocClasspath != null) {
getLog().debug("Setting Javadoc classpath: " + javadocClasspath);
System.setProperty("env.class.path", javadocClasspath); // The Javadoc code reads this env variable!
}
} catch (DependencyResolutionRequiredException ex) {
throw new Exception("Can't get test-scope classpath: " + ex.toString(), ex);
}
// Hurray for more logging abstractions!
Handler loggingAdapter = new Handler() {
Formatter formatter = new Formatter() {
@Override
public String format(LogRecord logRecord) {
return formatMessage(logRecord);
}
};
@Override
public void publish(LogRecord logRecord) {
if (logRecord.getLevel().equals(Level.SEVERE) && getLog().isErrorEnabled()) {
getLog().error(formatter.format(logRecord));
} else if (logRecord.getLevel().equals(Level.WARNING) && getLog().isWarnEnabled()) {
getLog().warn(formatter.format(logRecord));
} else if (logRecord.getLevel().equals(Level.INFO) && getLog().isInfoEnabled()) {
getLog().info(formatter.format(logRecord));
} else if (getLog().isDebugEnabled()) {
getLog().debug(formatter.format(logRecord));
}
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
};
loggingAdapter.setLevel(Level.ALL);
LoggingUtil.resetRootHandler(loggingAdapter);
LogManager.getLogManager().getLogger("").setLevel(Level.ALL);
// Check the configuration
for (File sourceDirectory : sourceDirectories) {
if (!sourceDirectory.canRead()) {
throw new Exception("Source directory not found or not readable: " + sourceDirectory);
}
if (!sourceDirectory.isDirectory()) {
throw new Exception("Source directory is not a directory: " + sourceDirectory);
}
}
if (packageNames.size() == 0) {
for (File sourceDirectory : sourceDirectories) {
// Default to all sub-directories in source directory
File[] subdirs = sourceDirectory.listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isDirectory() && file.getName().matches("[a-zA-Z_]+");
}
});
for (File subdir : subdirs) {
packageNames.add(subdir.getName());
}
// Filter duplicates
packageNames = new ArrayList(new LinkedHashSet(packageNames));
}
}
// Finally, do the work
return new XHTMLTemplateJavadocPipeline(sourceDirectories, packageNames, true, processXRefs);
}
public void copyManualResources(File destination) throws IOException {
final List<File> manualResources = new ArrayList();
getLog().info("Searching for manual resources to copy in: " + manualSourceDirectory);
File[] files = manualSourceDirectory.listFiles(new FileFilter() {
public boolean accept(File file) {
// Do not copy any .xhtml files, we assume that they all are "included" within the generated manual
return !file.getName().endsWith(".xhtml");
}
});
if (files != null)
manualResources.addAll(Arrays.asList(files));
for (File manualResource : manualResources) {
if (manualResource.getName().startsWith(".")) continue;
if (manualResource.isDirectory()) {
// Copy the directory only if it contains any non-XHTML files
for (File file : manualResource.listFiles()) {
if (file.getName().startsWith(".")) continue;
if (!file.getName().endsWith(".xhtml")) {
getLog().info("Copying directory recursively: " + manualResource);
FileUtils.copyDirectoryStructure(manualResource, new File(destination, manualResource.getName()));
break;
}
}
} else {
getLog().info("Copying file: " + manualResource);
FileUtils.copyFile(manualResource, new File(destination, manualResource.getName()));
}
}
}
public void copyDocFiles(File destination) throws IOException {
final List<File> docFiles = new ArrayList();
for (File sourceDirectory : sourceDirectories) {
getLog().info("Searching for doc-files to copy in: " + sourceDirectory);
IO.findFiles(sourceDirectory, new IO.FileFinder() {
public void found(File file) {
if (file.isDirectory() && file.getName().equals("doc-files")) {
File[] children = file.listFiles();
if (children != null)
docFiles.addAll(Arrays.asList(children));
}
}
});
}
File destinationDir = new File(destination, "doc-files");
if (docFiles.size() > 0 && !destinationDir.exists()) {
destinationDir.mkdir();
} else if (docFiles.size() > 0 && destinationDir.exists()) {
getLog().info("Cleaning old doc-files target directory: " + destinationDir);
FileUtils.deleteDirectory(destinationDir);
destinationDir.mkdir();
}
for (File docFile : docFiles) {
if (docFile.getName().startsWith(".")) continue;
File targetDocFile = new File(destinationDir, docFile.getName());
if (targetDocFile.exists()) {
throw new IOException("Duplicate doc-files detected, rename one: " + docFile.getName());
}
getLog().info("Copying doc-file to: " + targetDocFile);
FileUtils.copyFile(docFile, targetDocFile);
}
}
}