20. prosince 2009

Generování class diagramů

Class diagramy dnes umí vygenerovat mnoho nástrojů, ale přesto jsme raději nakonec použili vlastní řešení pro generování class diagramů. Mnohdy nám přišla nedostatečná kvalita vygenerovaných diagramů, jindy zase bylo málo možností konfigurace generování a nakonec se ukázalo, že bychom rádi celý proces generování class diagramů zautomatizovali, což u většiny nástrojů nebylo možné.

Takto vygenerované class diagramy používáme jednak pro vlastní potřebu, abychom se v určitém kódu sami lépe vyznali a pak je také daváme jako součást naší dokumentace, zejména pro rozhraní webových služeb.

Pro naše potřeby jsme použili knihovnu UmlGraph. Tato knihovna se stará o zjišťování vazeb mezi třídami a funguje na principu JavaDoc docletu. Buď je možné zvolit zcela automatický řežim, kdy knihovna se sama snaží podle konfigurace zjistit vazby mezi třídami a nebo je možné každou třídu doplnit v JavaDocu třídy speciálními tagy, např. pro vyjádření dědičnosti. Automatický řežim celkem funguje, ale není to 100% - pokud bych rád zobrazoval např. kardinalitu vazeb, pak musím použít tagy. My standardně používáme automatický řežim a jen jednou se nám stalo, že určité vazby se nezobrazily, tak jak jsme chtěli - potom jsme použili tagy a bylo vše ok.

UmlGraph pouze generuje DOT soubor, který je nutný následně zpracovat pomocí nástroje Graphviz, který teprve vykreslí výsledný obrázek. Bohužel Graphviz je nutné lokálně instalovat, což je snad jediná nevýhoda celého řešení. Je možné zase spoustu věcí nastavit dle vlastních potřeb, včetně výstupního formátu obrázku.



Přikládám ukázku kódu:

package cz.marbes.daisy.modules.generator.diagramgenerator;

import org.apache.commons.lang.StringUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.io.PrintWriter;

/**
* Generator class diagramu.
*
* <p>Generator je spousten pres {@link #main(String[]) main} metodu a generuje
* class diagram pro zadany balik (package) trid.
*
* @author <a href="mailto:petr.juza@marbes.cz">Petr Juza</a>
* @see <a href="http://www.graphviz.org">graphviz</a>
* @see <a href="http://www.umlgraph.org/">UML graph</a>
*/
public class ClassDiagramGenerator {

private final static String DOT_FILENAME = "graph.dot";
private static final String UMLGRAPH_MAIN_CLASS = "org.umlgraph.doclet.UmlGraph";

/**
* Vystupni format obrazku [png|gif]
*/
private static final String OUTPUT_IMAGE_FORMAT = "gif";


private ClassDiagramGenerator() {
}


/**
* Main metodu, vstupni bod pro generovani class diagramu.
*
* @param args Vstupni parametry generovani:
* <ol>
* <li>Package, pro ktery se ma generovat class diagram
* (napr. {@code cz.marbes.daisy.modules.aa.wscommon.komu.v1_1_2})
* <li>Vystupni adresar pro DOT soubor a vysledny obrazek
* <li>Absolutni cesta k adresari projektu se zdrojovymi soubory, kde je zadany package
* (napr. {@code /Volumes/Obelix/projects/daisy/apl/aa/trunk/aa-core/src/main/java})
* <li>Cesta ke graphviz DOT souboru (napr. {@code /usr/local/bin/dot})
* <li>Seznam nazvu trid (staci pouze nazev, nemusi byt vcetne baliku), ktere budou vynechany
* pri vykreslovani class diagramu. Jedn. tridy budou oddeleny carkou.
* </ol>
* @throws IllegalArgumentException pokud "nesedi" vstupni parametry
* @throws ClassNotFoundException pokud neni dostupna UmlGraph knihovna
*/
public static void main(String[] args) throws Exception {
// args = new String[]{
// "cz.marbes.daisy.modules.aa.wscommon.smlouvy.v1_1_3",
// "/Volumes/Obelix/projects/daisy/diagram_output",
// "/Volumes/Obelix/projects/daisy/apl/aa/trunk/aa-core/src/main/java",
// "/usr/local/bin/dot",
// "TypPripadService,SqlBuilderQuery,AaSqlBuilderHook"
// };

//kontrola existence UmlGraphu v classpath
try {
Class.forName(UMLGRAPH_MAIN_CLASS);
}
catch (ClassNotFoundException ex) {
throw new ClassNotFoundException("UmlGraph neni dustupny - " +
"pridejte knihovnu UmlGraph do classpath", ex);
}


//validace a zpracovani vstupnich hodnot
if (args == null
|| args.length < 4
|| args.length > 5) {
throw new IllegalArgumentException("Generator ocekava 4 povinne a 1 nepovinny vstupni parametr ...");
}

String packagePath = args[0];
String outputFolder = args[1];
String srcFolder = args[2];
File graphvizDotFile = new File(args[3]);

String excludeClasses = null;
if (args.length == 5) {
excludeClasses = args[4];
}

//kontrola existence graphviz DOT souboru
if (!graphvizDotFile.exists()
|| !graphvizDotFile.canExecute()) {
throw new IllegalArgumentException("Graphviz DOT soubor neexistuje ["
+ graphvizDotFile.getAbsolutePath() + "].");
}

System.out.println("--------------------------------------------------------------");
System.out.println("Generovani class diagramu pro - " + packagePath);
System.out.println("--------------------------------------------------------------");

//generovani
ClassDiagramGenerator generator = new ClassDiagramGenerator();
File diagramDotFile = generator.generateDotFile(packagePath, outputFolder, srcFolder, excludeClasses);

if (diagramDotFile != null) {
generator.generateImage(graphvizDotFile, diagramDotFile);
}

System.out.println("Konec generovani ...");
}


/**
* Generuje DOT soubor.
*
* <p>DOT soubor (format pro <a href="http://www.graphviz.org">graphviz</a>)
* je generovan knihovnou <a href="http://www.umlgraph.org/">UML graph</a>.
*
* @param packagePath Package, pro ktery se ma generovat class diagram
* (napr. {@code cz.marbes.daisy.modules.aa.wscommon.komu.v1_1_2}).
* @param outputFolder Vystupni adresar pro DOT soubor
* @param srcFolder Absolutni cesta k adresari se zdrojovymi soubory, kde je zadany package
* @param excludeClasses Seznam nazvu trid, ktere budou vynechany pri vykreslovani class diagramu.
* Jedn. tridy budou oddeleny carkou.
* @return generovany DOT soubor
*/
private File generateDotFile(String packagePath,
String outputFolder,
String srcFolder,
String excludeClasses) {
File fileDot = new File(outputFolder + File.separator + DOT_FILENAME);

if (StringUtils.isNotEmpty(excludeClasses)) {
//vymazou se mezery a nahradi se carky lomitky (, -> |)
excludeClasses = StringUtils.deleteWhitespace(excludeClasses);
excludeClasses = StringUtils.replaceChars(excludeClasses, ',', '|');
}

excludeClasses = StringUtils.trimToNull(excludeClasses);

//viz http://www.umlgraph.org/doc/cd-opt.html,
// http://java.sun.com/j2se/1.5.0/docs/tooldocs/windows/javadoc.html#javadocoptions
String[] args = new String[]{
"-all", //=-attributes -operations -visibility -types -enumerations -enumconstants
"-hide",
"^(java|com\\.sun|org.apache)" + (excludeClasses != null ? "|" + excludeClasses : ""),
"-inferrel",
"-inferdep",
"-inferdepinpackage",
"-collpackages",
"java.util.*",
"-verbose",
"-output",
fileDot.getAbsolutePath(), //napr.: "/Volumes/Obelix/D/graph.dot",
"-sourcepath",
srcFolder,
packagePath
};


//vytvoreni writeru kvuli logovani
PrintWriter errWriter = new PrintWriter(System.err);
PrintWriter warnWriter = new PrintWriter(System.out);
PrintWriter noticeWriter = new PrintWriter(System.out);

System.out.println("Generovani DOT souboru: ");
System.out.println(" Source folder - " + srcFolder);
System.out.println(" Package path - " + packagePath);
System.out.println(" Dot file - " + fileDot.getAbsolutePath());
System.out.println(" Exclude classes - " + excludeClasses);

//vygenerovani DOT souboru
com.sun.tools.javadoc.Main.execute("UmlGraph",
errWriter, warnWriter, noticeWriter, UMLGRAPH_MAIN_CLASS, args);

return fileDot;
}


/**
* Generuje vysledny obrazek class diagramu.
*
* @param graphvizDotFile Graphviz DOT soubor
* @param diagramDotFile DOT soubor ke zpracovani
* @throws Exception v pripade problemu pri generovani obrazku
*/
private void generateImage(File graphvizDotFile, File diagramDotFile) throws Exception {
try {
String[] args = new String[]{
graphvizDotFile.getAbsolutePath(),
"-T" + OUTPUT_IMAGE_FORMAT,
"-O",
diagramDotFile.getAbsolutePath()
};

System.out.println("Generovani obrazku: ");
System.out.println(" Graphviz DOT soubor - " + graphvizDotFile.getAbsolutePath());
System.out.println(" DOT soubor ke zpracovani - " + diagramDotFile.getAbsolutePath());

Process p = Runtime.getRuntime().exec(args);

BufferedReader reader = new BufferedReader(new InputStreamReader(p.getErrorStream()));
String line;
while ((line = reader.readLine()) != null) {
System.err.println(line);
}

int result = p.waitFor();
if (result != 0) {
System.err.println("Errors occured during running Graphviz - return code is " + result);
}
}
catch (Exception ex) {
System.err.println("Errors occured during running Graphviz - " + ex.getLocalizedMessage());
throw ex;
}
}
}


A ještě obecný ANT task pro spouštění generování diagramů:
    <target name="gen_diagram" description="Generovani class diagramu pro zadany package." depends="clean">
<input message="Zadejte package, pro ktery chcete generovat class diagram (napr. cz.marbes.daisy.modules.aa.wscommon.pvs.v1_1_1)?"
addproperty="packagePath" />

<input message="Zadejte cestu ke zdrojovym souborum relativni k projektu, kde je zadany package (napr. /apl/aa/trunk/aa-core/src/main/java)?"
addproperty="srcPath" />

<input message="(nepovinne) Zadejte seznam nazvu trid oddelenych carkou (nemusi byt vcetne baliku), ktere budou vynechany z class diagramu."
addproperty="excludeClasses" />

<java classname="cz.marbes.daisy.modules.generator.diagramgenerator.ClassDiagramGenerator"
classpathref="project.class.path" fork="true">
<arg value="${packagePath}"/>
<arg value="${diagram.output.folder}"/>
<arg value="${basedir}/${srcPath}"/>
<arg value="${graphviz.dot.path}"/>
<arg value="${excludeClasses}"/>
</java>
</target>

Žádné komentáře: