diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java index 1c6b616bb9b..be928d6cdfd 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java @@ -611,6 +611,16 @@ public class Main { listModules(args); } + + // Generate Module Graph File + if (args.getModuleGraphFilename() != null) + { + File outputFile = baseHome.getBaseFile(args.getModuleGraphFilename()); + System.out.printf("Generating GraphViz Graph of Jetty Modules at %s%n",baseHome.toShortForm(outputFile)); + ModuleGraphWriter writer = new ModuleGraphWriter(); + writer.config(args.getProperties()); + writer.write(args.getAllModules(),outputFile); + } // Show Command Line to execute Jetty if (args.isDryRun()) diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Module.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Module.java index c0563b72a18..4c57c3672ab 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Module.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Module.java @@ -30,17 +30,14 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.omg.CORBA.INITIALIZE; - /** * Represents a Module metadata, as defined in Jetty. */ -public class Module // extends TextFile +public class Module { public static class NameComparator implements Comparator { diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/ModuleGraphWriter.java b/jetty-start/src/main/java/org/eclipse/jetty/start/ModuleGraphWriter.java new file mode 100644 index 00000000000..7eacae35da5 --- /dev/null +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/ModuleGraphWriter.java @@ -0,0 +1,257 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.start; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.List; +import java.util.Properties; + +/** + * Generate a graphviz dot graph of the modules found + */ +public class ModuleGraphWriter +{ + private String colorModuleBg; + private String colorEnabledBg; + private String colorTransitiveBg; + private String colorCellBg; + private String colorHeaderBg; + private String colorModuleFont; + + public ModuleGraphWriter() + { + colorModuleBg = "#B8FFB8"; + colorEnabledBg = "#66FFCC"; + colorTransitiveBg = "#66CC66"; + colorCellBg = "#FFFFFF80"; + colorHeaderBg = "#00000020"; + colorModuleFont = "#888888"; + } + + public void config(Properties props) + { + String prefix = "jetty.graph."; + colorModuleBg = getProperty(props,prefix + "color.module.bg",colorModuleBg); + colorEnabledBg = getProperty(props,prefix + "color.enabled.bg",colorEnabledBg); + colorTransitiveBg = getProperty(props,prefix + "color.transitive.bg",colorTransitiveBg); + colorCellBg = getProperty(props,prefix + "color.cell.bg",colorCellBg); + colorHeaderBg = getProperty(props,prefix + "color.header.bg",colorHeaderBg); + colorModuleFont = getProperty(props,prefix + "color.font",colorModuleFont); + } + + private String getProperty(Properties props, String key, String defVal) + { + String val = props.getProperty(key,defVal); + if (val == null) + { + return defVal; + } + val = val.trim(); + if (val.length() <= 0) + { + return defVal; + } + return val; + } + + public void write(Modules modules, File outputFile) throws IOException + { + try (FileWriter writer = new FileWriter(outputFile,false); PrintWriter out = new PrintWriter(writer);) + { + writeHeaderMessage(out,outputFile); + + out.println(); + out.println("digraph modules {"); + + // Node Style + out.println(" node [color=gray, style=filled, shape=rectangle];"); + out.println(" node [fontname=\"Verdana\", size=\"20,20\"];"); + // Graph Style + out.println(" graph ["); + out.println(" concentrate=false,"); + out.println(" fontname=\"Verdana\","); + out.println(" fontsize = 20,"); + out.println(" rankdir = LR,"); + out.println(" ranksep = 1.5,"); + out.println(" nodesep = .5,"); + out.println(" style = bold,"); + out.println(" labeljust = l,"); + out.println(" label = \"Jetty Modules\","); + out.println(" ssize = \"20,40\""); + out.println(" ];"); + + List enabled = modules.resolveEnabled(); + + // Module Nodes + writeModules(out,modules,enabled); + + // Module Relationships + writeRelationships(out,modules,enabled); + + out.println("}"); + out.println(); + } + } + + private void writeHeaderMessage(PrintWriter out, File outputFile) + { + out.println("/*"); + out.println(" * GraphViz Graph of Jetty Modules"); + out.println(" * "); + out.println(" * Jetty: http://eclipse.org/jetty/"); + out.println(" * GraphViz: http://graphviz.org/"); + out.println(" * "); + out.println(" * To Generate Graph image using graphviz:"); + String filename = outputFile.getName(); + String basename = filename.substring(0,filename.indexOf('.')); + out.printf(" * $ dot -Tpng -Goverlap=false -o %s.png %s%n",basename,filename); + out.println(" */"); + } + + private void writeModuleDetailHeader(PrintWriter out, String header) + { + writeModuleDetailHeader(out,header,1); + } + + private void writeModuleDetailHeader(PrintWriter out, String header, int count) + { + out.printf(" "); + out.printf("",colorHeaderBg); + out.printf("%s%s",header,count > 1?"s":""); + out.println(""); + } + + private void writeModuleDetailLine(PrintWriter out, String line) + { + out.printf(" "); + StringBuilder escape = new StringBuilder(); + for(char c: line.toCharArray()) { + switch(c) { + case '<': escape.append("<"); break; + case '>': escape.append(">"); break; + default: + escape.append(c); + break; + } + } + + out.printf("%s%n",colorCellBg,escape.toString()); + } + + private void writeModuleNode(PrintWriter out, Module module, boolean resolved) + { + String color = colorModuleBg; + if (module.isEnabled()) + { + // specifically enabled by config + color = colorEnabledBg; + } + else if (resolved) + { + // enabled by transitive reasons + color = colorTransitiveBg; + } + + out.printf(" \"%s\" [ color=\"%s\" label=<",module.getName(),color); + out.printf("%n"); + out.printf(" %n",module.getName()); + + if (module.isEnabled()) + { + writeModuleDetailHeader(out,"ENABLED"); + for (String source : module.getSources()) + { + writeModuleDetailLine(out,"via: " + source); + } + } + else if (resolved) + { + writeModuleDetailHeader(out,"TRANSITIVE"); + } + + if (!module.getXmls().isEmpty()) + { + List xmls = module.getXmls(); + writeModuleDetailHeader(out,"XML",xmls.size()); + for (String xml : xmls) + { + writeModuleDetailLine(out,xml); + } + } + + if (!module.getLibs().isEmpty()) + { + List libs = module.getLibs(); + writeModuleDetailHeader(out,"LIB",libs.size()); + for (String lib : libs) + { + writeModuleDetailLine(out,lib); + } + } + + if (!module.getInitialise().isEmpty()) + { + List inis = module.getInitialise(); + writeModuleDetailHeader(out,"INI Template",inis.size()); + } + + out.printf("
%s
>];%n"); + } + + private void writeModules(PrintWriter out, Modules allmodules, List enabled) + { + out.println(); + out.println(" /* Modules */"); + out.println(); + + out.println(" node [ labeljust = l ];"); + + for (int depth = 0; depth <= allmodules.getMaxDepth(); depth++) + { + out.println(); + Collection depthModules = allmodules.getModulesAtDepth(depth); + if (depthModules.size() > 0) + { + out.printf(" /* Level %d */%n",depth); + out.println(" { rank = same;"); + for (Module module : depthModules) + { + boolean resolved = enabled.contains(module); + writeModuleNode(out,module,resolved); + } + out.println(" }"); + } + } + } + + private void writeRelationships(PrintWriter out, Modules modules, List enabled) + { + for (Module module : modules) + { + for (Module parent : module.getParentEdges()) + { + out.printf(" \"%s\" -> \"%s\";%n",module.getName(),parent.getName()); + } + } + } +} diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java index bac372bb2e7..8b217647332 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java @@ -37,6 +37,7 @@ import java.util.Stack; public class Modules implements Iterable { private Map modules = new HashMap<>(); + private int maxDepth = -1; private Set asNameSet(Set moduleSet) { @@ -83,6 +84,7 @@ public class Modules implements Iterable for (Module child : module.getChildEdges()) { child.setDepth(Math.max(depth,child.getDepth())); + this.maxDepth = Math.max(this.maxDepth,child.getDepth()); } // Dive down @@ -118,7 +120,7 @@ public class Modules implements Iterable for (String optionalParentName : module.getOptionalParentNames()) { Module optional = get(optionalParentName); - if (optional==null) + if (optional == null) { System.err.printf("WARNING: module not found [%s]%n",optionalParentName); } @@ -242,6 +244,24 @@ public class Modules implements Iterable return modules.get(name); } + public int getMaxDepth() + { + return maxDepth; + } + + public Set getModulesAtDepth(int depth) + { + Set ret = new HashSet<>(); + for (Module module : modules.values()) + { + if (module.getDepth() == depth) + { + ret.add(module); + } + } + return ret; + } + @Override public Iterator iterator() { diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java b/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java index 57f4cee27dd..d3001317a1c 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java @@ -79,6 +79,7 @@ public class StartArgs private List jvmArgs = new ArrayList<>(); private List moduleIni = new ArrayList<>(); private List moduleStartIni = new ArrayList<>(); + private String moduleGraphFilename; private Modules allModules; // Should the server be run? @@ -439,6 +440,11 @@ public class StartArgs return System.getProperty("main.class",mainclass); } + public String getModuleGraphFilename() + { + return moduleGraphFilename; + } + public List getModuleIni() { return moduleIni; @@ -711,6 +717,13 @@ public class StartArgs return; } + if (arg.startsWith("--write-module-graph=")) + { + this.moduleGraphFilename = getValue(arg); + run = false; + return; + } + // Start property (syntax similar to System property) if (arg.startsWith("-D")) { diff --git a/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt b/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt index 6817c422e8e..c72edbb5543 100644 --- a/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt +++ b/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt @@ -82,6 +82,13 @@ Module Management: enabled in the ${jetty.base}/start.ini using the same techniques. + --write-module-graph= + Create a graphviz *.dot file of the module graph as it + exists for the active ${jetty.base}. + See http://graphviz.org/ for details on how to post-process + this file into the output best suited for your needs. + + Startup / Shutdown Command Line: -------------------------------- diff --git a/jetty-start/src/test/java/org/eclipse/jetty/start/ModuleGraphWriterTest.java b/jetty-start/src/test/java/org/eclipse/jetty/start/ModuleGraphWriterTest.java new file mode 100644 index 00000000000..a8d42e1f24c --- /dev/null +++ b/jetty-start/src/test/java/org/eclipse/jetty/start/ModuleGraphWriterTest.java @@ -0,0 +1,60 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.start; + +import static org.hamcrest.Matchers.*; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.TestingDir; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +public class ModuleGraphWriterTest +{ + @SuppressWarnings("unused") + private final static List TEST_SOURCE = Collections.singletonList(""); + + @Rule + public TestingDir testdir = new TestingDir(); + + @Test + public void testGenerate_NothingEnabled() throws IOException + { + File homeDir = MavenTestingUtils.getTestResourceDir("usecases/home"); + File baseDir = testdir.getEmptyDir(); + BaseHome basehome = new BaseHome(homeDir,baseDir); + + Modules modules = new Modules(); + modules.registerAll(basehome); + modules.buildGraph(); + + File outputFile = new File(baseDir,"graph.dot"); + + ModuleGraphWriter writer = new ModuleGraphWriter(); + writer.write(modules,outputFile); + + Assert.assertThat("Output File Exists",outputFile.exists(),is(true)); + } +}