/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.camel.maven;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.siterenderer.Renderer;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
/**
* Runs Camel embedded with META-INF/services/*.xml spring files to try create
* DOT files for the routing rules, then converts the DOT files into another
* format such as PNG
*
* @version $Revision: 726747 $
* @goal dot
* @requiresDependencyResolution runtime
* @phase prepare-package
* @execute phase="test-compile"
* @see <a href="http://www.graphviz.org/">GraphViz</a>
*/
public class DotMojo extends AbstractMavenReport {
public static final String[] DEFAULT_GRAPHVIZ_OUTPUT_TYPES = {"png", "svg", "cmapx"};
/**
* Subdirectory for report.
*/
protected static final String SUBDIRECTORY = "cameldoc";
//
// For running Camel embedded
// -------------------------------------------------------------------------
//
/**
* The duration to run the application for which by default is in
* milliseconds. A value <= 0 will run forever.
* Adding a s indicates seconds - eg "5s" means 5 seconds.
*
* @parameter expression="2s"
*/
protected String duration;
/**
* Whether we should boot up camel with the META-INF/services/*.xml to
* generate the DOT file
*
* @parameter expression="true"
*/
protected boolean runCamel;
/**
* Should we try run the DOT executable on the generated .DOT file to
* generate images
*
* @parameter expression="true"
*/
protected boolean useDot;
/**
* The classpath based application context uri that spring wants to get.
*
* @parameter expression="${camel.applicationContextUri}"
*/
protected String applicationContextUri;
/**
* The filesystem based application context uri that spring wants to get.
*
* @parameter expression="${camel.fileApplicationContextUri}"
*/
protected String fileApplicationContextUri;
/**
* The main class to execute.
*
* @parameter expression="${camel.mainClass}"
* default-value="org.apache.camel.spring.Main"
* @required
*/
private String mainClass;
/**
* Reference to Maven 2 Project.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* Base output directory.
*
* @parameter expression="${project.build.directory}"
* @required
*/
private File buildDirectory;
/**
* Base output directory for reports.
*
* @parameter default-value="${project.build.directory}/site/cameldoc"
* @required
*/
private File outputDirectory;
/**
* In the case of multiple camel contexts, setting aggregate == true will
* aggregate all into a monolithic context, otherwise they will be processed
* independently.
*
* @parameter
*/
private String aggregate;
/**
* GraphViz executable location; visualization (images) will be generated
* only if you install this program and set this property to the executable
* dot (dot.exe on Win).
*
* @parameter expression="dot"
*/
private String executable;
/**
* Graphviz output types. Default is png. Possible values: png, jpg, gif,
* svg.
*
* @required
*/
private String graphvizOutputType;
/**
* Graphviz output types. Possible values: png, jpg, gif, svg.
*
* @parameter
*/
private String[] graphvizOutputTypes;
/**
* Doxia SiteRender.
*
* @component
*/
private Renderer renderer;
private String indexHtmlContent;
/**
* @param locale report locale.
* @return report description.
* @see org.apache.maven.reporting.MavenReport#getDescription(Locale)
*/
public String getDescription(final Locale locale) {
return getBundle(locale).getString("report.dot.description");
}
/**
* @see org.apache.maven.reporting.MavenReport#getName(Locale)
*/
public String getName(final Locale locale) {
return getBundle(locale).getString("report.dot.name");
}
public String getOutputName() {
return SUBDIRECTORY + "/index";
}
public String getAggregate() {
return aggregate;
}
public void setAggregate(String aggregate) {
this.aggregate = aggregate;
}
public boolean isUseDot() {
return useDot;
}
public void setUseDot(boolean useDot) {
this.useDot = useDot;
}
public void execute() throws MojoExecutionException {
this.execute(this.buildDirectory, Locale.getDefault());
try {
writeIndexHtmlFile(outputDirectory, "index.html", indexHtmlContent);
} catch (IOException e) {
throw new MojoExecutionException("Failed: " + e, e);
}
}
protected void executeReport(final Locale locale) throws MavenReportException {
try {
this.execute(this.outputDirectory, locale);
Sink kitchenSink = getSink();
if (kitchenSink != null) {
kitchenSink.rawText(indexHtmlContent);
} else {
writeIndexHtmlFile(outputDirectory, "index.html", indexHtmlContent);
}
} catch (Exception e) {
final MavenReportException ex = new MavenReportException(e.getMessage());
ex.initCause(e.getCause());
throw ex;
}
}
/**
* Executes DOT generator.
*
* @param outputDir report output directory.
* @param locale report locale.
* @throws MojoExecutionException if there were any execution errors.
*/
protected void execute(final File outputDir, final Locale locale) throws MojoExecutionException {
try {
runCamelEmbedded(outputDir);
} catch (DependencyResolutionRequiredException e) {
throw new MojoExecutionException("Failed: " + e, e);
}
outputDir.mkdirs();
List<File> files = new ArrayList<File>();
appendFiles(files, outputDirectory);
if (graphvizOutputTypes == null) {
if (graphvizOutputType == null) {
graphvizOutputTypes = DEFAULT_GRAPHVIZ_OUTPUT_TYPES;
} else {
graphvizOutputTypes = new String[] {graphvizOutputType};
}
}
try {
Set<String> contextNames = new HashSet<String>();
for (File file : files) {
String contextName = file.getParentFile().getName();
contextNames.add(contextName);
}
boolean multipleCamelContexts = contextNames.size() > 1;
int size = files.size();
for (int i = 0; i < size; i++) {
File file = files.get(i);
String contextName = null;
if (multipleCamelContexts) {
contextName = file.getParentFile().getName();
}
getLog().info("Generating contextName: " + contextName + " file: " + file + "");
generate(i, file, contextName);
}
if (multipleCamelContexts) {
// lets generate an index page which lists each indiviual
// CamelContext file
StringWriter buffer = new StringWriter();
PrintWriter out = new PrintWriter(buffer);
out.println("<h1>Camel Contexts</h1>");
out.println();
out.println("<ul>");
for (String contextName : contextNames) {
out.print(" <li><a href='");
out.print(contextName);
out.print("/routes.html'>");
out.print(contextName);
out.println("</a></li>");
}
out.println("</ul>");
indexHtmlContent = buffer.toString();
}
} catch (CommandLineException e) {
throw new MojoExecutionException("Failed: " + e, e);
} catch (IOException e) {
throw new MojoExecutionException("Failed: " + e, e);
}
}
private void generate(int index, File file, String contextName) throws CommandLineException,
MojoExecutionException, IOException {
StringWriter buffer = new StringWriter();
PrintWriter out = new PrintWriter(buffer);
printHtmlHeader(out, contextName);
printHtmlFileHeader(out, file);
for (int j = 0; j < graphvizOutputTypes.length; j++) {
String format = graphvizOutputTypes[j];
String generated = convertFile(file, format);
if (format.equals("cmapx") && generated != null) {
// lets include the generated file inside the html
addFileToBuffer(out, new File(generated));
}
}
printHtmlFileFooter(out, file);
printHtmlFooter(out);
String content = buffer.toString();
String name = file.getName();
if (name.equalsIgnoreCase("routes.dot") || index == 0) {
indexHtmlContent = content;
}
int idx = name.lastIndexOf(".");
if (idx >= 0) {
name = name.substring(0, idx);
name += ".html";
}
writeIndexHtmlFile(file.getParentFile(), name, content);
}
protected void runCamelEmbedded(File outputDir) throws DependencyResolutionRequiredException {
if (runCamel) {
// default path, but can be overridden by configuration
if (applicationContextUri != null) {
getLog().info("Running Camel embedded to load Spring XML files from classpath: " + applicationContextUri);
} else if (fileApplicationContextUri != null) {
getLog().info("Running Camel embedded to load Spring XML files from file path: " + fileApplicationContextUri);
} else {
getLog().info("Running Camel embedded to load Spring XML files from default path: META-INF/spring/*.xml");
}
List list = project.getTestClasspathElements();
getLog().debug("Using classpath: " + list);
EmbeddedMojo mojo = new EmbeddedMojo();
mojo.setClasspathElements(list);
mojo.setDotEnabled(true);
mojo.setMainClass(mainClass);
if ("true".equals(getAggregate())) {
mojo.setDotAggregationEnabled(true);
}
mojo.setOutputDirectory(outputDirectory.getAbsolutePath());
mojo.setDuration(duration);
mojo.setLog(getLog());
mojo.setPluginContext(getPluginContext());
mojo.setApplicationContextUri(applicationContextUri);
mojo.setFileApplicationContextUri(fileApplicationContextUri);
try {
mojo.executeWithoutWrapping();
} catch (Exception e) {
getLog().error("Failed to run Camel embedded: " + e, e);
}
}
}
protected void writeIndexHtmlFile(File dir, String fileName, String content) throws IOException {
// File dir = outputDirectory;
dir.mkdirs();
File html = new File(dir, fileName);
PrintWriter out = null;
try {
out = new PrintWriter(new FileWriter(html));
out.println("<html>");
out.println("<head>");
out.println("</head>");
out.println("<body>");
out.println();
if (content == null) {
out.write("<p>No EIP diagrams available</p>");
} else {
out.write(content);
}
out.println("</body>");
out.println("</html>");
} finally {
String description = "Failed to close html output file";
close(out, description);
}
}
protected void printHtmlHeader(PrintWriter out, String contextName) {
if (contextName != null) {
out.println("<h1>EIP Patterns for CamelContext: " + contextName + "</h1>");
} else {
out.println("<h1>Camel EIP Patterns</h1>");
}
out.println();
}
protected void printHtmlFileHeader(PrintWriter out, File file) {
out.println("<p>");
out.println(" <img src='" + removeFileExtension(file.getName()) + ".png' usemap='#CamelRoutes'>");
}
protected void printHtmlFileFooter(PrintWriter out, File file) {
out.println(" </img>");
out.println("</p>");
out.println();
}
protected void printHtmlFooter(PrintWriter out) {
out.println();
}
protected void close(Closeable closeable, String description) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
getLog().warn(description + ": " + e);
}
}
}
protected String convertFile(File file, String format) throws CommandLineException {
Log log = getLog();
if (!useDot) {
log.info("DOT generation disabled.");
return null;
} else {
if (dotHelpExitCode() != 0) {
log.info("'dot -?' execution failed so DOT generation disabled.");
return null;
}
}
if (this.executable == null || this.executable.length() == 0) {
log.warn("Parameter <executable/> was not set in the pom.xml. Skipping conversion.");
return null;
}
String generatedFileName = removeFileExtension(file.getAbsolutePath()) + "." + format;
Commandline cl = new Commandline();
cl.setExecutable(executable);
cl.createArgument().setValue("-T" + format);
cl.createArgument().setValue("-o");
cl.createArgument().setValue(generatedFileName);
cl.createArgument().setValue(file.getAbsolutePath());
log.debug("executing: " + cl.toString());
CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer();
CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer();
int exitCode = CommandLineUtils.executeCommandLine(cl, stdout, stderr);
String output = stdout.getOutput();
if (output.length() > 0) {
log.debug(output);
}
String errOutput = stderr.getOutput();
if (errOutput.length() > 0) {
log.warn(errOutput);
}
return generatedFileName;
}
private int dotHelpExitCode() throws CommandLineException {
Commandline cl = new Commandline();
cl.setExecutable(executable);
cl.createArgument().setValue("-?");
CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer();
CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer();
return CommandLineUtils.executeCommandLine(cl, stdout, stderr);
}
protected String removeFileExtension(String name) {
int idx = name.lastIndexOf(".");
if (idx > 0) {
return name.substring(0, idx);
} else {
return name;
}
}
private void appendFiles(List<File> output, File file) {
if (file.isDirectory()) {
appendDirectory(output, file);
} else {
if (isValid(file)) {
output.add(file);
}
}
}
private void appendDirectory(List<File> output, File dir) {
File[] files = dir.listFiles();
for (File file : files) {
appendFiles(output, file);
}
}
private boolean isValid(File file) {
String name = file.getName().toLowerCase();
return name.endsWith(".dot");
}
private void addFileToBuffer(PrintWriter out, File file) throws MojoExecutionException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
while (true) {
String line = reader.readLine();
if (line == null) {
break;
} else {
out.println(line);
}
}
} catch (IOException e) {
throw new MojoExecutionException("Failed: " + e, e);
} finally {
close(reader, "cmapx file");
}
}
/**
* Gets resource bundle for given locale.
*
* @param locale locale
* @return resource bundle
*/
protected ResourceBundle getBundle(final Locale locale) {
return ResourceBundle.getBundle("camel-maven-plugin", locale, this.getClass().getClassLoader());
}
protected Renderer getSiteRenderer() {
return this.renderer;
}
protected String getOutputDirectory() {
return this.outputDirectory.getAbsolutePath();
}
protected MavenProject getProject() {
return this.project;
}
}