Skip to content

Commit 28a09b5

Browse files
committed
Replace Graphviz/DOT with a Java implementation
1 parent f6e0efe commit 28a09b5

4 files changed

Lines changed: 99 additions & 87 deletions

File tree

api/build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,32 @@ dependencies {
381381
)
382382
)
383383

384+
BuildUtils.addExternalDependency(
385+
project,
386+
new ExternalDependency(
387+
"org.graphper:graph-support-core:${graphSupportVersion}",
388+
"graph-support-core",
389+
"graph-support",
390+
"https://github.com/jamisonjiang/graph-support",
391+
ExternalDependency.APACHE_2_LICENSE_NAME,
392+
ExternalDependency.APACHE_2_LICENSE_URL,
393+
"Graphviz Java API",
394+
)
395+
)
396+
397+
BuildUtils.addExternalDependency(
398+
project,
399+
new ExternalDependency(
400+
"org.graphper:graph-support-dot:${graphSupportVersion}",
401+
"graph-support-dot",
402+
"graph-support",
403+
"https://github.com/jamisonjiang/graph-support",
404+
ExternalDependency.APACHE_2_LICENSE_NAME,
405+
ExternalDependency.APACHE_2_LICENSE_URL,
406+
"DOT parsing support",
407+
)
408+
)
409+
384410
BuildUtils.addExternalDependency(
385411
project,
386412
new ExternalDependency(

api/src/org/labkey/api/module/ModuleDependencySorter.java

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@
1818

1919
import org.apache.commons.collections4.MultiValuedMap;
2020
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
21-
import org.apache.logging.log4j.LogManager;
2221
import org.apache.logging.log4j.Logger;
22+
import org.graphper.api.GraphResource;
23+
import org.graphper.api.Graphviz;
24+
import org.graphper.parser.DotParser;
2325
import org.junit.Assert;
2426
import org.junit.Test;
2527
import org.labkey.api.collections.CaseInsensitiveHashSet;
26-
import org.labkey.api.util.DotRunner;
2728
import org.labkey.api.util.FileUtil;
2829
import org.labkey.api.util.Pair;
30+
import org.labkey.api.util.logging.LogHelper;
2931

3032
import java.io.File;
3133
import java.util.ArrayList;
@@ -34,12 +36,12 @@
3436
import java.util.stream.Collectors;
3537

3638
/**
37-
* Orders modules so that each module will always be after all of the modules it depends on.
38-
* User: jeckels
39-
* Date: Jun 6, 2006
39+
* Orders modules so that each module will always be after all the modules it depends on.
4040
*/
4141
public class ModuleDependencySorter
4242
{
43+
private static final Logger LOG = LogHelper.getLogger(ModuleDependencySorter.class, "Module dependency information");
44+
4345
public List<Module> sortModulesByDependencies(List<Module> modules)
4446
{
4547
List<Pair<Module, Set<String>>> dependencies = new ArrayList<>();
@@ -101,7 +103,7 @@ public List<Module> sortModulesByDependencies(List<Module> modules)
101103
if (module.getName().equalsIgnoreCase("core"))
102104
{
103105
result.remove(i);
104-
result.add(0, module);
106+
result.addFirst(module);
105107
break;
106108
}
107109
}
@@ -128,41 +130,36 @@ private Module findModuleWithoutDependencies(List<Pair<Module, Set<String>>> dep
128130
throw new IllegalArgumentException("Module '" + moduleName + "' (" + entry.getKey().getClass().getName() + ") is listed as being dependent on itself.");
129131
}
130132

131-
StringBuilder sb = new StringBuilder();
132-
for (Pair<Module, Set<String>> dependencyInfo : dependencies)
133-
{
134-
if (!sb.isEmpty())
135-
{
136-
sb.append(", ");
137-
}
138-
sb.append(dependencyInfo.getKey().getName());
139-
}
133+
String involved = dependencies.stream()
134+
.map(pair -> pair.getKey().getName())
135+
.collect(Collectors.joining(", "));
140136

141137
// Generate an SVG diagram that shows all remaining dependencies
142138
graphModuleDependencies(dependencies, "involved");
143139

144-
throw new IllegalArgumentException("Unable to resolve module dependencies. The following modules are somehow involved: " + sb);
140+
throw new IllegalArgumentException("Unable to resolve module dependencies. The following modules are somehow involved: " + involved);
145141
}
146142

147143

148144
private void graphModuleDependencies(List<Pair<Module, Set<String>>> dependencies, @SuppressWarnings("SameParameterValue") String adjective)
149145
{
150-
Logger log = LogManager.getLogger(ModuleDependencySorter.class);
151-
152146
try
153147
{
154148
File dir = FileUtil.getTempDirectory();
155149
String dot = buildDigraph(dependencies);
150+
Graphviz graph = DotParser.parse(dot);
156151
File svgFile = FileUtil.createTempFile("modules", ".svg", dir);
157-
DotRunner runner = new DotRunner(dir, dot);
158-
runner.addSvgOutput(svgFile);
159-
runner.execute();
160152

161-
log.info("For a diagram of " + adjective + " module dependencies, see " + svgFile.getAbsolutePath());
153+
try (GraphResource resource = graph.toSvg())
154+
{
155+
resource.save(svgFile.getParent(), svgFile.getName());
156+
}
157+
158+
LOG.info("For a diagram of {} module dependencies, see {}", adjective, svgFile.getAbsolutePath());
162159
}
163160
catch (Exception e)
164161
{
165-
log.error("Error running dot", e);
162+
LOG.error("Error running dot", e);
166163
}
167164
}
168165

core/src/org/labkey/core/security/SecurityController.java

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import org.apache.commons.collections4.IteratorUtils;
2121
import org.apache.commons.lang3.StringUtils;
2222
import org.apache.poi.ss.usermodel.Sheet;
23+
import org.graphper.api.Graphviz;
24+
import org.graphper.parser.DotParser;
2325
import org.jetbrains.annotations.NotNull;
2426
import org.jetbrains.annotations.Nullable;
2527
import org.json.JSONObject;
@@ -119,9 +121,6 @@
119121
import org.labkey.api.security.roles.RoleManager;
120122
import org.labkey.api.security.roles.SiteAdminRole;
121123
import org.labkey.api.settings.AppProps;
122-
import org.labkey.api.util.DotRunner;
123-
import org.labkey.api.util.FileUtil;
124-
import org.labkey.api.util.HelpTopic;
125124
import org.labkey.api.util.HtmlString;
126125
import org.labkey.api.util.HtmlStringBuilder;
127126
import org.labkey.api.util.PageFlowUtil;
@@ -149,7 +148,6 @@
149148
import org.springframework.validation.ObjectError;
150149
import org.springframework.web.servlet.ModelAndView;
151150

152-
import java.io.File;
153151
import java.io.IOException;
154152
import java.io.Writer;
155153
import java.sql.SQLException;
@@ -2099,50 +2097,16 @@ public ApiResponse execute(GroupDiagramForm form, BindException errors) throws E
20992097
}
21002098
else
21012099
{
2102-
String graph = GroupManager.getGroupGraphDot(groups, getUser(), form.getHideUnconnected());
2103-
File dir = FileUtil.getTempDirectory();
2104-
File svgFile = null;
2105-
2106-
try
2107-
{
2108-
svgFile = FileUtil.createTempFile("groups", ".svg", dir);
2109-
svgFile.deleteOnExit();
2110-
DotRunner runner = new DotRunner(dir, graph);
2111-
runner.addSvgOutput(svgFile);
2112-
runner.execute();
2113-
String svg = PageFlowUtil.getFileContentsAsString(svgFile);
2114-
2115-
int idx = svg.indexOf("<svg");
2116-
html = -1 != idx ? svg.substring(idx) : "Graphviz failed to generate this group diagram";
2117-
}
2118-
catch (IOException ioe)
2119-
{
2120-
if (ioe.getMessage().startsWith("Cannot run program \"dot\""))
2121-
{
2122-
html = "This feature requires graphviz to be installed; ";
2123-
2124-
if (getUser().hasRootPermission(AdminOperationsPermission.class))
2125-
html += "see " + new HelpTopic("thirdPartyCode").getSimpleLinkHtml("the LabKey installation instructions") + " for more information.";
2126-
else
2127-
html += "contact a server administrator about this problem.";
2128-
}
2129-
else
2130-
{
2131-
throw ioe;
2132-
}
2133-
}
2134-
finally
2135-
{
2136-
if (null != svgFile)
2137-
svgFile.delete();
2138-
}
2100+
String dot = GroupManager.getGroupGraphDot(groups, getUser(), form.getHideUnconnected());
2101+
Graphviz graph = DotParser.parse(dot);
2102+
html = graph.toSvgStr();
21392103
}
21402104

21412105
return new ApiSimpleResponse("html", html);
21422106
}
21432107
}
21442108

2145-
private static class GroupDiagramForm
2109+
public static class GroupDiagramForm
21462110
{
21472111
private boolean _hideUnconnected = false;
21482112

@@ -2159,7 +2123,7 @@ public boolean getHideUnconnected()
21592123
}
21602124

21612125
@RequiresPermission(AdminPermission.class)
2162-
public class FolderAccessAction extends SimpleViewAction<FolderAccessForm>
2126+
public static class FolderAccessAction extends SimpleViewAction<FolderAccessForm>
21632127
{
21642128
@Override
21652129
public ModelAndView getView(FolderAccessForm form, BindException errors)
@@ -2409,7 +2373,7 @@ controller.new GroupPermissionAction(),
24092373
new UpdatePermissionsAction(),
24102374
new ShowRegistrationEmailAction(),
24112375
new GroupDiagramAction(),
2412-
controller.new FolderAccessAction()
2376+
new FolderAccessAction()
24132377
);
24142378

24152379
// @RequiresPermission(UserManagementPermission.class)

pipeline/src/org/labkey/pipeline/analysis/AnalysisController.java

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import org.apache.commons.lang3.StringUtils;
2323
import org.apache.logging.log4j.LogManager;
2424
import org.apache.logging.log4j.Logger;
25+
import org.graphper.api.Graphviz;
26+
import org.graphper.parser.DotParser;
2527
import org.jetbrains.annotations.Nullable;
2628
import org.json.JSONArray;
2729
import org.json.JSONObject;
@@ -658,25 +660,45 @@ private DOM.Renderable generateGraph(@Nullable TaskPipeline<?> pipeline)
658660
}
659661

660662
File svgFile = null;
661-
try
662-
{
663-
File dir = FileUtil.getTempDirectory();
664-
String dot = buildDigraph(pipeline);
665-
svgFile = FileUtil.createTempFile("pipeline", ".svg", dir);
666-
DotRunner runner = new DotRunner(dir, dot);
667-
runner.addSvgOutput(svgFile);
668-
runner.execute();
669-
return HtmlString.unsafe(PageFlowUtil.getFileContentsAsString(svgFile));
670-
}
671-
catch (Exception e)
672-
{
673-
LOG.error("Error running dot", e);
674-
}
675-
finally
663+
File dir = FileUtil.getTempDirectory();
664+
String dot = buildDigraph(pipeline, true);
665+
666+
if (null != dot)
676667
{
677-
if (svgFile != null)
678-
svgFile.delete();
668+
String htmlOld = "Oops... error with DotRunner!";
669+
try
670+
{
671+
svgFile = FileUtil.createTempFile("pipeline", ".svg", dir);
672+
DotRunner runner = new DotRunner(dir, dot);
673+
runner.addSvgOutput(svgFile);
674+
runner.execute();
675+
htmlOld = PageFlowUtil.getFileContentsAsString(svgFile);
676+
}
677+
catch (Exception e)
678+
{
679+
LOG.error("Error running dot", e);
680+
}
681+
finally
682+
{
683+
if (svgFile != null)
684+
svgFile.delete();
685+
}
686+
687+
String html = "Oops... error with DotParser!";
688+
try
689+
{
690+
dot = buildDigraph(pipeline, false);
691+
Graphviz graph = DotParser.parse(dot);
692+
html = graph.toSvgStr();
693+
}
694+
catch (Exception e)
695+
{
696+
LOG.error("Error with DotParser!", e);
697+
}
698+
699+
return HtmlString.unsafe(htmlOld + "<br>" + html);
679700
}
701+
680702
return null;
681703
}
682704

@@ -692,7 +714,7 @@ private DOM.Renderable generateGraph(@Nullable TaskPipeline<?> pipeline)
692714
* +---------+----------+
693715
* </pre>
694716
*/
695-
private String buildDigraph(TaskPipeline<?> pipeline)
717+
private String buildDigraph(TaskPipeline<?> pipeline, boolean dotRunner)
696718
{
697719
TaskId[] progression = pipeline.getTaskProgression();
698720
if (progression == null)
@@ -701,6 +723,9 @@ private String buildDigraph(TaskPipeline<?> pipeline)
701723
StringBuilder sb = new StringBuilder();
702724
sb.append("digraph pipeline {\n");
703725

726+
if (!dotRunner)
727+
sb.append("style=\"invis\";\nmargin=\"0,0\";\n"); // TODO: graph-support doesn't seem to respect "transparent"
728+
704729
// First, add all the nodes
705730
for (TaskId taskId : progression)
706731
{
@@ -730,7 +755,7 @@ private String buildDigraph(TaskPipeline<?> pipeline)
730755
if (factory instanceof CommandTaskImpl.Factory f)
731756
{
732757
sb.append(StringUtils.join(
733-
Collections2.transform(f.getInputPaths().keySet(), (Function<String, Object>) input -> escapeDotFieldLabel(input) + "\\l"),
758+
Collections2.transform(f.getInputPaths().keySet(), (Function<String, Object>) this::escapeDotFieldLabel), // + "\\l"), TODO: Add this back once graph-support supports it
734759
" | "));
735760
}
736761
else
@@ -747,7 +772,7 @@ private String buildDigraph(TaskPipeline<?> pipeline)
747772
{
748773

749774
sb.append(StringUtils.join(
750-
Collections2.transform(f.getOutputPaths().keySet(), (Function<String, Object>) input -> escapeDotFieldLabel(input) + "\\r"),
775+
Collections2.transform(f.getOutputPaths().keySet(), (Function<String, Object>) this::escapeDotFieldLabel), // + "\\r"), TODO: Add this back once graph-support supports it
751776
" | "));
752777
}
753778
else

0 commit comments

Comments
 (0)