// Copyright (c) 2006 - 2008, Markus Strauch. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. package net.sf.sdedit.taglet; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import net.sf.sdedit.config.Configuration; import net.sf.sdedit.config.ConfigurationManager; import net.sf.sdedit.diagram.Diagram; import net.sf.sdedit.text.TextHandler; import net.sf.sdedit.ui.ImagePaintDevice; import com.sun.javadoc.Doc; import com.sun.javadoc.PackageDoc; import com.sun.javadoc.ProgramElementDoc; import com.sun.javadoc.Tag; import com.sun.tools.doclets.internal.toolkit.taglets.Taglet; import com.sun.tools.doclets.internal.toolkit.taglets.TagletOutput; import com.sun.tools.doclets.internal.toolkit.taglets.TagletWriter; /** * This is a taglet that generates sequence diagrams from the contents of * <tt>@sequence.diagram</tt> tags. The diagrams are saved as PNG files in a child * directory of the javadoc destination directory (as * specified by the -d option). Inside the html pages * generated by javadoc, the files are referenced using a * relative path, so the references remain valid if the pages * are moved - for example, uploaded to a webserver. * <p> * If the first line of the tag content is "quoted" it will be * used as title for the diagram instead of the default * "Sequence Diagram:" * <p> * <tt>@sequence.diagram</tt> are no inline tags and they can be used inside * classes, and inside package documentation. * * @sequence.diagram "Processing of <tt>@sequence.diagram</tt> tags" user:Actor javadoc:Javadoc[a] sourceFile:File * /docFile:File taglet:SequenceTaglet * * user:javadoc.generateDocumentation() [c:loop for all source files] * javadoc:sourceFile.read() javadoc:docFile.new() [c:loop "sequence.diagram" * tag found] javadoc:output=taglet.getTagletOutput(tag) * javadoc:docFile.append(output) [/c] [/c] * * @author Markus Strauch * @author �ystein Lunde */ public class SequenceTaglet implements Taglet { private static final String tagName = "sequence.diagram"; /** * The name of the child directory of the javadoc destination directory * where the png files are stored. */ private static final String imageSubDirectory = "sequence-diagrams"; /** * The directory where the png files are stored. */ private File diagramDirectory; /** * Counts how often an image base name has been used */ private Map<String, Integer> nameCounter; /** * Registers an instance of this taglet class. * * @param tagletMap * used for registering (maps tag names onto taglets) */ public static void register(Map<String, Taglet> tagletMap) { SequenceTaglet tag = new SequenceTaglet(); Taglet t = (Taglet) tagletMap.get(tag.getName()); if (t != null) { tagletMap.remove(tag.getName()); } tagletMap.put(tag.getName(), tag); } private SequenceTaglet() { nameCounter = new HashMap<String, Integer>(); } private String getPathToJavadocDestination(Tag tag) throws SequenceTagletException { PackageDoc pdoc = null; if (tag.holder() instanceof PackageDoc) { pdoc = (PackageDoc) tag.holder(); } else { ProgramElementDoc pedoc = (ProgramElementDoc) tag.holder(); pdoc = pedoc.containingPackage(); } if (pdoc.allClasses() == null || pdoc.allClasses().length == 0) { throw new SequenceTagletException( "Cowardly refusing to generate a sequence diagram for an empty package", null); } String qualifiedName = pdoc.allClasses()[0].qualifiedName(); String path = ""; for (int i = 0; i < qualifiedName.length(); i++) { if (qualifiedName.charAt(i) == '.') { path += "../"; } } return path; } /** * Returns the base name of the image to be generated, which is the name of * the holder of the tag containing the diagram specification, followed by a * unique number. * * @param tag * a sequence.diagram tag * @return the name of its holder, followed by a unique number */ private String getImageName(Tag tag) { String name = tag.holder().name(); Integer count = nameCounter.get(name); if (count == null) { count = 1; } else { count++; } nameCounter.put(name, count); return name + "-" + count; } /** * Creates a sequence diagram image from a part of the contents of a * sequence.diagram tag, saves it in the image directory and returns HTML * code that references the image. * * @param path * the path to the directory where the diagram image is to be * stored (usually a path going upwards, i. e. containing ../'s) * @param imageBaseName * the base name of the image to be stored * @param source * string array containing the lines of the diagram specification * @return the output that is to appear on the javadoc page (i. e. the * <img> tag referencing the image * @throws SequenceTagletException * if the creation of the diagram fails */ private String generateOutput(String path, String imageBaseName, String[] source) throws SequenceTagletException { if (source == null || source.length == 0) { return ""; } // Set custom diagram title if first line is quoted String diagramTitle = null; if (source[0].trim().matches("^[\"'].*[\"']$")) { // Set the title and remove the title quote diagramTitle = source[0].replaceAll("[\"']", ""); // Remove the custom title from the sd spec source[0] = ""; } StringBuffer buffer = new StringBuffer(); for (String string : source) { string = string.trim(); if (string.startsWith("<") && string.endsWith(">")) { continue; } buffer.append(string + "\n"); } String specification = buffer.toString().trim(); if (specification.length() == 0) { return ""; } Configuration conf = ConfigurationManager.createNewDefaultConfiguration().getDataObject(); conf.setHeadWidth(25); conf.setMainLifelineWidth(5); conf.setSubLifelineWidth(5); conf.setThreaded(true); conf.setGlue(3); ImagePaintDevice device = new ImagePaintDevice(); TextHandler handler = new TextHandler(specification); try { new Diagram(conf, handler, device).generate(); } catch (Exception e) { int error = handler.getLineNumber(); StringBuffer code = new StringBuffer("<br><tt>"); for (int i = 0; i < source.length; i++) { String html = source[i].replaceAll("&", "&").replaceAll( "<", "<").replaceAll(">", ">").replaceAll("\"", """); if (i == error) { html = "<FONT COLOR=\"red\"><U><B>" + html + "</B></U></FONT>"; } code.append(html + "<br>"); } throw new SequenceTagletException( "Malformed diagram specification: " + e.getMessage(), "<DT><HR><B>Sequence Diagram:</B></DT>" + "<DD><B>Could not create sequence diagram: " + "<font color=\"red\">" + e.getMessage() + "</font></B>" + code.toString() + "</DD>"); } String fileName = imageBaseName + ".png"; File imageFile = new File(diagramDirectory, fileName); try { device.saveImage(imageFile.getAbsolutePath()); } catch (IOException ioe) { throw new SequenceTagletException("Could not save diagram image: " + ioe.getMessage(), ""); } if (diagramTitle == null) { diagramTitle = "Sequence Diagram " + imageBaseName; } String imageUrl = path + imageSubDirectory + "/" + fileName; String anchor = "<A name=\"" + imageBaseName + "\"/>"; return "<DT><HR><B>" + anchor + diagramTitle + ":</B><P></DT>" + "<DD><img src='" + imageUrl + "'></DD>"; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#getName() */ public String getName() { return tagName; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inConstructor() */ public boolean inConstructor() { return true; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inField() */ public boolean inField() { return true; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inMethod() */ public boolean inMethod() { return true; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inOverview() */ public boolean inOverview() { return false; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inPackage() */ public boolean inPackage() { return true; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#inType() */ public boolean inType() { return true; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#isInlineTag() */ public boolean isInlineTag() { return false; } private String toString(Tag tag, TagletWriter writer) { String output; try { String path = getPathToJavadocDestination(tag); output = generateOutput(path, getImageName(tag), tag.text().split( "\n")); } catch (SequenceTagletException ste) { writer.configuration().message.warning("doclet.in", ste .getMessage(), tag.holder().name()); return ste.output; } return output; } private String toString(Tag[] tags, TagletWriter writer) { if (tags != null && tags.length > 0) { String output = ""; for (Tag tag : tags) { try { output += generateOutput(getPathToJavadocDestination(tag), getImageName(tag), tag.text().split("\n")); } catch (SequenceTagletException ste) { writer.configuration().message.warning("doclet.in", ste .getMessage(), tags[0].holder().name()); output += ste.output; } } return output; } else { return null; } } private void setDestinationDirectory(String dir) { File baseDir = new File(dir); diagramDirectory = new File(baseDir, imageSubDirectory); diagramDirectory.mkdirs(); } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#getTagletOutput(com.sun.javadoc.Tag, * com.sun.tools.doclets.internal.toolkit.taglets.TagletWriter) */ public TagletOutput getTagletOutput(Tag tag, TagletWriter writer) throws IllegalArgumentException { if (diagramDirectory == null) { setDestinationDirectory(writer.configuration().destDirName); } TagletOutput out = writer.getTagletOutputInstance(); out.setOutput(toString(tag, writer)); return out; } /** * @see com.sun.tools.doclets.internal.toolkit.taglets.Taglet#getTagletOutput(com.sun.javadoc.Doc, * com.sun.tools.doclets.internal.toolkit.taglets.TagletWriter) */ public TagletOutput getTagletOutput(Doc holder, TagletWriter writer) throws IllegalArgumentException { if (diagramDirectory == null) { setDestinationDirectory(writer.configuration().destDirName); } TagletOutput out = writer.getTagletOutputInstance(); Tag[] tags = holder.tags(getName()); if (tags.length == 0) { return null; } out.setOutput(toString(tags, writer)); return out; } private static class SequenceTagletException extends Exception { /** * Appears on the HTML page */ String output; /** * * @param warning * the warning that is sent to the doclet * @param output * the output that appears on the HTML page */ SequenceTagletException(String warning, String output) { super(warning); this.output = output; } } }