001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.maven;
018    
019    import java.io.BufferedReader;
020    import java.io.Closeable;
021    import java.io.File;
022    import java.io.FileReader;
023    import java.io.FileWriter;
024    import java.io.IOException;
025    import java.io.PrintWriter;
026    import java.io.StringWriter;
027    import java.util.ArrayList;
028    import java.util.HashSet;
029    import java.util.List;
030    import java.util.Locale;
031    import java.util.ResourceBundle;
032    import java.util.Set;
033    
034    import org.apache.maven.artifact.DependencyResolutionRequiredException;
035    import org.apache.maven.doxia.sink.Sink;
036    import org.apache.maven.doxia.siterenderer.Renderer;
037    import org.apache.maven.plugin.MojoExecutionException;
038    import org.apache.maven.plugin.logging.Log;
039    import org.apache.maven.project.MavenProject;
040    import org.apache.maven.reporting.AbstractMavenReport;
041    import org.apache.maven.reporting.MavenReportException;
042    import org.codehaus.plexus.util.cli.CommandLineException;
043    import org.codehaus.plexus.util.cli.CommandLineUtils;
044    import org.codehaus.plexus.util.cli.Commandline;
045    
046    /**
047     * Runs Camel embedded with META-INF/services/*.xml spring files to try create
048     * DOT files for the routing rules, then converts the DOT files into another
049     * format such as PNG
050     *
051     * @version $Revision: 22844 $
052     * @goal dot
053     * @requiresDependencyResolution test
054     * @phase prepare-package
055     * @execute phase="test-compile"
056     * @see <a href="http://www.graphviz.org/">GraphViz</a>
057     */
058    public class DotMojo extends AbstractMavenReport {
059        public static final String[] DEFAULT_GRAPHVIZ_OUTPUT_TYPES = {"png", "svg", "cmapx"};
060        /**
061         * Subdirectory for report.
062         */
063        protected static final String SUBDIRECTORY = "cameldoc";
064        //
065        // For running Camel embedded
066        // -------------------------------------------------------------------------
067        //
068        /**
069         * The duration to run the application for which by default is in
070         * milliseconds. A value <= 0 will run forever.
071         * Adding a s indicates seconds - eg "5s" means 5 seconds.
072         *
073         * @parameter expression="2s"
074         */
075        protected String duration;
076    
077        /**
078         * Whether we should boot up camel with the META-INF/services/*.xml to
079         * generate the DOT file
080         *
081         * @parameter expression="true"
082         */
083        protected boolean runCamel;
084        
085        /**
086         * Should we try run the DOT executable on the generated .DOT file to
087         * generate images
088         *
089         * @parameter expression="true"
090         */
091        protected boolean useDot;
092    
093        /**
094         * The classpath based application context uri that spring wants to get.
095         *
096         * @parameter expression="${camel.applicationContextUri}"
097         */
098        protected String applicationContextUri;
099    
100        /**
101         * The filesystem based application context uri that spring wants to get.
102         *
103         * @parameter expression="${camel.fileApplicationContextUri}"
104         */
105        protected String fileApplicationContextUri;
106    
107        /**
108         * The main class to execute.
109         *
110         * @parameter expression="${camel.mainClass}"
111         *            default-value="org.apache.camel.spring.Main"
112         * @required
113         */
114        private String mainClass;
115        
116        /**
117         * Reference to Maven 2 Project.
118         *
119         * @parameter expression="${project}"
120         * @required
121         * @readonly
122         */
123        private MavenProject project;
124    
125        /**
126         * Base output directory.
127         *
128         * @parameter expression="${project.build.directory}"
129         * @required
130         */
131        private File buildDirectory;
132    
133        /**
134         * Base output directory for reports.
135         *
136         * @parameter default-value="${project.build.directory}/site/cameldoc"
137         * @required
138         */
139        private File outputDirectory;
140    
141    
142        /**
143         * In the case of multiple camel contexts, setting aggregate == true will
144         * aggregate all into a monolithic context, otherwise they will be processed
145         * independently.
146         *
147         * @parameter
148         */
149        private String aggregate;
150    
151        /**
152         * GraphViz executable location; visualization (images) will be generated
153         * only if you install this program and set this property to the executable
154         * dot (dot.exe on Win).
155         *
156         * @parameter expression="dot"
157         */
158        private String executable;
159    
160        /**
161         * Graphviz output types. Default is png. Possible values: png, jpg, gif,
162         * svg.
163         *
164         * @required
165         */
166        private String graphvizOutputType;
167    
168        /**
169         * Graphviz output types. Possible values: png, jpg, gif, svg.
170         *
171         * @parameter
172         */
173        private String[] graphvizOutputTypes;
174    
175        /**
176         * Doxia SiteRender.
177         *
178         * @component
179         */
180        private Renderer renderer;
181    
182        private String indexHtmlContent;
183    
184        /**
185         * @param locale report locale.
186         * @return report description.
187         * @see org.apache.maven.reporting.MavenReport#getDescription(Locale)
188         */
189        public String getDescription(final Locale locale) {
190            return getBundle(locale).getString("report.dot.description");
191        }
192    
193        /**
194         * @see org.apache.maven.reporting.MavenReport#getName(Locale)
195         */
196        public String getName(final Locale locale) {
197            return getBundle(locale).getString("report.dot.name");
198        }
199    
200        public String getOutputName() {
201            return SUBDIRECTORY + "/index";
202        }
203    
204        public String getAggregate() {
205            return aggregate;
206        }
207    
208        public void setAggregate(String aggregate) {
209            this.aggregate = aggregate;
210        }
211    
212        public boolean isUseDot() {
213            return useDot;
214        }
215    
216        public void setUseDot(boolean useDot) {
217            this.useDot = useDot;
218        }
219    
220        public void execute() throws MojoExecutionException {
221            this.execute(this.buildDirectory, Locale.getDefault());
222            try {
223                writeIndexHtmlFile(outputDirectory, "index.html", indexHtmlContent);
224            } catch (IOException e) {
225                throw new MojoExecutionException("Failed: " + e, e);
226            }
227        }
228    
229        protected void executeReport(final Locale locale) throws MavenReportException {
230            try {
231                this.execute(this.outputDirectory, locale);
232    
233                Sink kitchenSink = getSink();
234                if (kitchenSink != null) {
235                    kitchenSink.rawText(indexHtmlContent);
236                } else {
237                    writeIndexHtmlFile(outputDirectory, "index.html", indexHtmlContent);
238                }
239            } catch (Exception e) {
240                final MavenReportException ex = new MavenReportException(e.getMessage());
241                ex.initCause(e.getCause());
242                throw ex;
243            }
244        }
245    
246        /**
247         * Executes DOT generator.
248         *
249         * @param outputDir report output directory.
250         * @param locale report locale.
251         * @throws MojoExecutionException if there were any execution errors.
252         */
253        protected void execute(final File outputDir, final Locale locale) throws MojoExecutionException {
254    
255            try {
256                runCamelEmbedded(outputDir);
257            } catch (DependencyResolutionRequiredException e) {
258                throw new MojoExecutionException("Failed: " + e, e);
259            }
260            outputDir.mkdirs();
261    
262            List<File> files = new ArrayList<File>();
263            appendFiles(files, outputDirectory);
264    
265            if (graphvizOutputTypes == null) {
266                if (graphvizOutputType == null) {
267                    graphvizOutputTypes = DEFAULT_GRAPHVIZ_OUTPUT_TYPES;
268                } else {
269                    graphvizOutputTypes = new String[] {graphvizOutputType};
270                }
271            }
272            try {
273                Set<String> contextNames = new HashSet<String>();
274                for (File file : files) {
275                    String contextName = file.getParentFile().getName();
276                    contextNames.add(contextName);
277                }
278    
279                boolean multipleCamelContexts = contextNames.size() > 1;
280                int size = files.size();
281                for (int i = 0; i < size; i++) {
282                    File file = files.get(i);
283                    String contextName = null;
284                    if (multipleCamelContexts) {
285                        contextName = file.getParentFile().getName();
286                    }
287    
288                    getLog().info("Generating contextName: " + contextName + " file: " + file + "");
289    
290                    generate(i, file, contextName);
291                }
292    
293                if (multipleCamelContexts) {
294                    // lets generate an index page which lists each indiviual
295                    // CamelContext file
296                    StringWriter buffer = new StringWriter();
297                    PrintWriter out = new PrintWriter(buffer);
298    
299                    out.println("<h1>Camel Contexts</h1>");
300                    out.println();
301    
302                    out.println("<ul>");
303                    for (String contextName : contextNames) {
304                        out.print("  <li><a href='");
305                        out.print(contextName);
306                        out.print("/routes.html'>");
307                        out.print(contextName);
308                        out.println("</a></li>");
309                    }
310                    out.println("</ul>");
311                    indexHtmlContent = buffer.toString();
312                }
313            } catch (CommandLineException e) {
314                throw new MojoExecutionException("Failed: " + e, e);
315            } catch (IOException e) {
316                throw new MojoExecutionException("Failed: " + e, e);
317            }
318        }
319    
320        private void generate(int index, File file, String contextName) throws CommandLineException,
321            MojoExecutionException, IOException {
322    
323            StringWriter buffer = new StringWriter();
324            PrintWriter out = new PrintWriter(buffer);
325            printHtmlHeader(out, contextName);
326            printHtmlFileHeader(out, file);
327            for (int j = 0; j < graphvizOutputTypes.length; j++) {
328                String format = graphvizOutputTypes[j];
329                String generated = convertFile(file, format);
330    
331                if (format.equals("cmapx") && generated != null) {
332                    // lets include the generated file inside the html
333                    addFileToBuffer(out, new File(generated));
334                }
335            }
336            printHtmlFileFooter(out, file);
337            printHtmlFooter(out);
338    
339            String content = buffer.toString();
340            String name = file.getName();
341            if (name.equalsIgnoreCase("routes.dot") || index == 0) {
342                indexHtmlContent = content;
343            }
344            int idx = name.lastIndexOf(".");
345            if (idx >= 0) {
346                name = name.substring(0, idx);
347                name += ".html";
348            }
349            writeIndexHtmlFile(file.getParentFile(), name, content);
350        }
351    
352        protected void runCamelEmbedded(File outputDir) throws DependencyResolutionRequiredException {
353            if (runCamel) {
354                // default path, but can be overridden by configuration
355                if (applicationContextUri != null) {
356                    getLog().info("Running Camel embedded to load Spring XML files from classpath: " + applicationContextUri);
357                } else if (fileApplicationContextUri != null) {
358                    getLog().info("Running Camel embedded to load Spring XML files from file path: " + fileApplicationContextUri);
359                } else {
360                    getLog().info("Running Camel embedded to load Spring XML files from default path: META-INF/spring/*.xml");
361                }
362    
363                List list = project.getTestClasspathElements();
364                getLog().debug("Using classpath: " + list);
365    
366                EmbeddedMojo mojo = new EmbeddedMojo();
367                mojo.setClasspathElements(list);
368                mojo.setDotEnabled(true);
369                mojo.setMainClass(mainClass);
370                if ("true".equals(getAggregate())) {
371                    mojo.setDotAggregationEnabled(true);
372                }
373                mojo.setOutputDirectory(outputDirectory.getAbsolutePath());
374                mojo.setDuration(duration);
375                mojo.setLog(getLog());
376                mojo.setPluginContext(getPluginContext());
377                mojo.setApplicationContextUri(applicationContextUri);
378                mojo.setFileApplicationContextUri(fileApplicationContextUri);
379                try {
380                    mojo.executeWithoutWrapping();
381                } catch (Exception e) {
382                    getLog().error("Failed to run Camel embedded: " + e, e);
383                }
384            }
385        }
386    
387        protected void writeIndexHtmlFile(File dir, String fileName, String content) throws IOException {
388            // File dir = outputDirectory;
389            dir.mkdirs();
390            File html = new File(dir, fileName);
391            PrintWriter out = null;
392            try {
393                out = new PrintWriter(new FileWriter(html));
394                out.println("<html>");
395                out.println("<head>");
396                out.println("</head>");
397                out.println("<body>");
398                out.println();
399                if (content == null) {
400                    out.write("<p>No EIP diagrams available</p>");
401                } else {
402                    out.write(content);
403                }
404                out.println("</body>");
405                out.println("</html>");
406            } finally {
407                String description = "Failed to close html output file";
408                close(out, description);
409            }
410        }
411    
412        protected void printHtmlHeader(PrintWriter out, String contextName) {
413            if (contextName != null) {
414                out.println("<h1>EIP Patterns for CamelContext: " + contextName + "</h1>");
415            } else {
416                out.println("<h1>Camel EIP Patterns</h1>");
417            }
418            out.println();
419        }
420    
421        protected void printHtmlFileHeader(PrintWriter out, File file) {
422            out.println("<p>");
423            out.println("  <img src='" + removeFileExtension(file.getName()) + ".png' usemap='#CamelRoutes'>");
424        }
425    
426        protected void printHtmlFileFooter(PrintWriter out, File file) {
427            out.println("  </img>");
428            out.println("</p>");
429            out.println();
430        }
431    
432        protected void printHtmlFooter(PrintWriter out) {
433            out.println();
434        }
435    
436        protected void close(Closeable closeable, String description) {
437            if (closeable != null) {
438                try {
439                    closeable.close();
440                } catch (IOException e) {
441                    getLog().warn(description + ": " + e);
442                }
443            }
444        }
445    
446        protected String convertFile(File file, String format) throws CommandLineException {
447            Log log = getLog();
448            if (!useDot) {
449                log.info("DOT generation disabled.");
450                return null;
451            } else {            
452                if (dotHelpExitCode() != 0) {
453                    log.info("'dot -?' execution failed so DOT generation disabled.");
454                    return null;
455                }
456            }
457            if (this.executable == null || this.executable.length() == 0) {
458                log.warn("Parameter <executable/> was not set in the pom.xml.  Skipping conversion.");
459                return null;
460            }
461    
462            String generatedFileName = removeFileExtension(file.getAbsolutePath()) + "." + format;
463            Commandline cl = new Commandline();
464            cl.setExecutable(executable);
465            cl.createArgument().setValue("-T" + format);
466            cl.createArgument().setValue("-o");
467            cl.createArgument().setValue(generatedFileName);
468            cl.createArgument().setValue(file.getAbsolutePath());
469    
470            log.debug("executing: " + cl.toString());
471    
472            CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer();
473            CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer();
474    
475            int exitCode = CommandLineUtils.executeCommandLine(cl, stdout, stderr);
476    
477            String output = stdout.getOutput();
478            if (output.length() > 0) {
479                log.debug(output);
480            }
481            String errOutput = stderr.getOutput();
482            if (errOutput.length() > 0) {
483                log.warn(errOutput);
484            }
485            return generatedFileName;
486        }
487    
488        private int dotHelpExitCode() throws CommandLineException {
489            Commandline cl = new Commandline();
490            cl.setExecutable(executable);
491            cl.createArgument().setValue("-?");
492    
493            CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer();
494            CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer();
495    
496            return CommandLineUtils.executeCommandLine(cl, stdout, stderr);
497        }
498    
499        protected String removeFileExtension(String name) {
500            int idx = name.lastIndexOf(".");
501            if (idx > 0) {
502                return name.substring(0, idx);
503            } else {
504                return name;
505            }
506        }
507    
508        private void appendFiles(List<File> output, File file) {
509            if (file.isDirectory()) {
510                appendDirectory(output, file);
511            } else {
512                if (isValid(file)) {
513                    output.add(file);
514                }
515            }
516        }
517    
518        private void appendDirectory(List<File> output, File dir) {
519            File[] files = dir.listFiles();
520            for (File file : files) {
521                appendFiles(output, file);
522            }
523        }
524    
525        private boolean isValid(File file) {
526            String name = file.getName().toLowerCase();
527            return name.endsWith(".dot");
528        }
529    
530        private void addFileToBuffer(PrintWriter out, File file) throws MojoExecutionException {
531            BufferedReader reader = null;
532            try {
533                reader = new BufferedReader(new FileReader(file));
534                while (true) {
535                    String line = reader.readLine();
536                    if (line == null) {
537                        break;
538                    } else {
539                        out.println(line);
540                    }
541                }
542            } catch (IOException e) {
543                throw new MojoExecutionException("Failed: " + e, e);
544            } finally {
545                close(reader, "cmapx file");
546            }
547        }
548    
549        /**
550         * Gets resource bundle for given locale.
551         *
552         * @param locale locale
553         * @return resource bundle
554         */
555        protected ResourceBundle getBundle(final Locale locale) {
556            return ResourceBundle.getBundle("camel-maven-plugin", locale, this.getClass().getClassLoader());
557        }
558    
559        protected Renderer getSiteRenderer() {
560            return this.renderer;
561        }
562    
563        protected String getOutputDirectory() {
564            return this.outputDirectory.getAbsolutePath();
565        }
566    
567        protected MavenProject getProject() {
568            return this.project;
569        }
570    }